diff --git a/docs/README.md b/docs/README.md index 77d7496..df25721 100644 --- a/docs/README.md +++ b/docs/README.md @@ -46,8 +46,8 @@ XRP is the native currency on the XRP Ledger network. The balance is tracked in 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Transactor.cpp#L413-L414) -[^xrp-reserve]: Account reserve calculation: [`Fees.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/Fees.h#L24-L33) +[^xrp-fees]: Transaction fee deduction: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/Transactor.cpp#L443) +[^xrp-reserve]: Account reserve calculation: [`Fees.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/Fees.h#L37-L46) ## 2.2. IOU @@ -61,10 +61,10 @@ Issuer accounts can set the RequireAuth flag[^require-auth] to control which tru 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/detail/ledger_entries.macro#L283-L287) -[^transfer-rate]: TransferRate field on AccountRoot: [`ledger_entries.macro`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/detail/ledger_entries.macro#L142) -[^require-auth]: RequireAuth flag on AccountRoot: [`LedgerFormats.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/LedgerFormats.h#L109-L110) -[^default-ripple]: DefaultRipple flag on AccountRoot: [`LedgerFormats.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/LedgerFormats.h#L115-L116) +[^quality-fields]: Quality fields on RippleState: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/ledger_entries.macro#L284-L288) +[^transfer-rate]: TransferRate field on AccountRoot: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/ledger_entries.macro#L142) +[^require-auth]: RequireAuth flag on AccountRoot: [`LedgerFormats.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/LedgerFormats.h#L130) +[^default-ripple]: DefaultRipple flag on AccountRoot: [`LedgerFormats.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/LedgerFormats.h#L135) ## 2.3. MPT diff --git a/docs/amms/README.md b/docs/amms/README.md index c2df7fa..ec85d09 100644 --- a/docs/amms/README.md +++ b/docs/amms/README.md @@ -106,7 +106,7 @@ The **spot price** is the weighted ratio of pool balances representing the excha 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)). +`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: @@ -265,7 +265,7 @@ Voting power is determined by the number of LP tokens held: an account holding 3 **Vote Slot Management:** -- Maximum 8 vote slots (defined by `VOTE_MAX_SLOTS`)[^vote-max-slots] +- 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 @@ -337,7 +337,7 @@ The `AMM` ledger entry (type `ltAMM = 0x0079`)[^amm-ledger-entry] tracks the sta 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Indexes.cpp#L68) +[^amm-namespace]: AMM namespace constant: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -398,25 +398,23 @@ The AMM's `Account` field references a pseudo-account[^pseudo-account-creation] - 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L242) -[^disabled-master-key]: Master key disabled with lsfDisableMaster flag: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1174-L1175) -[^ammid-field]: AMMID field set in pseudo-account: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L242) -[^pseudo-account-address]: Pseudo-account address generation: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1076-L1090) -[^zero-credit-limit]: LP token trustline created with zero balance: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L260-L264) -[^mpt-amm-flag]: MPToken created with lsfMPTAMM flag: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L313) -[^mpt-authorized-flag]: MPToken authorized flag when RequireAuth is set: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L314-L322) +[^pseudo-account-creation]: Pseudo-account creation for AMM: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AccountRootHelpers.cpp#L244) +[^ammid-field]: AMMID field set in pseudo-account: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251) +[^pseudo-account-address]: Pseudo-account address generation: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1076-L1090) +[^collision-avoidance-algo]: Collision-avoidance algorithm for pseudo-account address: [`AccountRootHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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 `maxAccountAttempts`) +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. @@ -424,16 +422,16 @@ For each attempt `i` (0 to 255): ``` hash = SHA512-Half(i, parentLedgerHash, ammLedgerEntryKey) -accountID = RIPEMD160(hash) +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::zero` (all zeros account ID) +6. **Failure**: If all 256 attempts find collisions, return `beast::kZero` (all zeros account ID) **Failure Handling:** -If `pseudoAccountAddress()` returns `beast::zero` (indicating all 256 attempts failed): +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 @@ -459,7 +457,7 @@ For an AMM with USD/XRP: The `AMM` ledger entry itself does not require an owner reserve. However: -- Creating an AMM requires a base transaction fee of one owner reserve (`view.fees().increment`) +- 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 @@ -470,14 +468,14 @@ AMMs create `RippleState` entries (trust lines) for: - The LP token issued by the AMM All AMM trust lines: -- Have zero credit limits (to prevent unsolicited deposits) +- 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L346-L348) -[^ripplestate-no-quality]: Quality modifiers only set if non-zero: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1478-L1484) +[^ripplestate-amm-flag]: Trust line marked with lsfAMMNode flag: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -486,15 +484,15 @@ See [Trust Lines Documentation](../trust_lines/README.md#21-ripplestate-ledger-e 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) -- May have the `lsfMPTAuthorized` flag[^mptoken-authorized-flag] set if the MPTokenIssuance requires authorization (`lsfMPTRequireAuth`) +- 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L313) -[^mptoken-authorized-flag]: MPToken authorized flag when RequireAuth is set: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L314-L322) -[^mptoken-creation]: MPToken creation for AMM pseudo-account: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L324-L327) -[^mptoken-no-owner-count]: AMM owner count not adjusted for MPToken: [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L328-L329) +[^mptoken-amm-flag]: MPToken created with lsfMPTAMM flag: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L311) +[^mptoken-creation]: MPToken creation for AMM pseudo-account: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L318) +[^mptoken-no-owner-count]: AMM owner count not adjusted for MPToken: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L320-L321) See [MPTokens Documentation](../mpts/README.md) for complete details on `MPToken` ledger entries. @@ -508,34 +506,34 @@ Several AMM transactions (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`) us - `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: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1996-L2003) -[^xrp-negative-check]: Negative amount check with fixAMMv1_1: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1930-L1935) +[^xrp-insufficient-balance]: Insufficient XRP balance check: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L825-L830) **For IOU transfers:** -- Calls `rippleSendIOU()`[^iou-ripple-send] which then calls `rippleCreditIOU()`[^iou-ripple-credit] and may call `issueIOU()`[^iou-issue] or `redeemIOU()`[^iou-redeem] +- 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] - - `tecINTERNAL`: Trust line doesn't exist after transfer[^iou-no-line] + - `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] -- With [featureDeletableAccounts](https://xrpl.org/resources/known-amendments#deletableaccounts): Errors from `rippleCreditIOU()` are propagated instead of being ignored[^iou-deletable-accounts]. These include: +- 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]: rippleSendIOU function: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1870-L1919) -[^iou-ripple-credit]: rippleCreditIOU function: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1716-L1864) -[^iou-issue]: issueIOU function: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2335-L2434) -[^iou-redeem]: redeemIOU function: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2436-L2509) -[^iou-dir-full]: Owner directory full check: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1439-L1448) -[^iou-insuf-reserve]: Insufficient reserve to create trust line: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1255-L1256) -[^iou-no-line]: Trust line doesn't exist after attempting redeem: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2499-L2508) -[^iou-null-account]: Receiver account SLE null check: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1842-L1844), [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1454-L1455) -[^iou-no-target]: Peer account doesn't exist check: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1461-L1464) -[^iou-deletable-accounts]: featureDeletableAccounts error propagation: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1893-L1894) -[^iou-bad-ledger]: Directory removal failure in trustDelete: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1628-L1646) +[^iou-ripple-send]: directSendNoLimitIOU function: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L694-L743) +[^iou-ripple-credit]: directSendNoFeeIOU function: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L550-L690) +[^iou-issue]: issueIOU function: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L385-L476) +[^iou-redeem]: redeemIOU function: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L479-L547) +[^iou-dir-full]: Owner directory full check: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L654-L655) +[^iou-no-line]: Trust line doesn't exist after attempting redeem: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L538-L547) +[^iou-null-account]: Receiver account SLE null check: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L668-L670), [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L239-L241) +[^iou-deletable-accounts]: IOU send error propagation: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L716-L717) +[^iou-bad-ledger]: Directory removal failure in trustDelete: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L308-L318) **For MPT transfer:** - `tecOBJECT_NOT_FOUND`: MPT issuance object doesn't exist[^mpt-object-not-found] @@ -546,21 +544,20 @@ Several AMM transactions (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`, `AMMBid`) us - 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: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2169-L2171) -[^mpt-path-dry-send]: MPT transfer exceeds MaximumAmount (rippleSendMPT): [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2187-L2189) -[^mpt-path-dry-credit]: MPT transfer exceeds MaximumAmount (rippleCreditMPT): [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2091-L2093) -[^mpt-insufficient-funds]: Sender MPToken balance insufficient: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2103-L2105) -[^mpt-sender-no-auth]: Sender MPToken entry missing: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2115-L2116) -[^mpt-receiver-no-auth]: Receiver MPToken entry missing: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2143-L2144) -[^mpt-internal]: Outstanding amount less than redemption: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2126-L2127) -[^amm-ledger-entry]: AMM ledger entry type definition: [`ledger_entries.macro`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/detail/ledger_entries.macro#L372-L383) -[^amm-keylet]: AMM keylet computation using asset pair: [`Indexes.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Indexes.cpp#L451-L489) -[^amm-keylet-hash]: AMM keylet hash with namespace `0x0041` and fields `(account, currency)` per asset: [`Indexes.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Indexes.cpp#L459-L464) -[^amm-lpt-currency]: LP token currency code generation: [`AMMCore.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/AMMCore.cpp#L23-L46) -[^amm-lp-tokens-calc]: Initial LP token calculation: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L5-L17) -[^vote-max-slots]: Maximum vote slots constant: [`AMMCore.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/AMMCore.h#L25) -[^vote-weighted-average]: Weighted average fee calculation: [`AMMVote.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMVote.cpp#L191-L193) -[^vote-min-tokens]: Vote slot replacement logic: [`AMMVote.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMVote.cpp#L125-L134) +[^mpt-object-not-found]: MPT issuance not found: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1161-L1163) +[^mpt-path-dry-send]: MPT transfer exceeds MaximumAmount (directSendNoLimitMPT): [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1084-L1085) +[^mpt-insufficient-funds]: Sender MPToken balance insufficient: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1095-L1097) +[^mpt-sender-no-auth]: Sender MPToken entry missing: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1102-L1104) +[^mpt-receiver-no-auth]: Receiver MPToken entry missing: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1136-L1138) +[^mpt-internal]: Outstanding amount less than redemption: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Indexes.cpp#L432-L437) +[^amm-lp-tokens-calc]: Initial LP token calculation: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L45-L54) +[^vote-max-slots]: Maximum vote slots constant: [`AMMCore.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/AMMCore.h#L24) +[^vote-weighted-average]: Weighted average fee calculation: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L205-L207) +[^vote-min-tokens]: Vote slot replacement logic: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -583,7 +580,7 @@ The two amounts can be in any order - the AMM will automatically order them as ` **Static validation**[^ammcreate-static-validation] -[^ammcreate-static-validation]: Static validation (preflight): [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L18-L63) +[^ammcreate-static-validation]: Static validation (preflight): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L42-L85) - `temDISABLED`: - [AMM](https://xrpl.org/resources/known-amendments#amm) amendment is not enabled @@ -592,19 +589,20 @@ The two amounts can be in any order - the AMM will automatically order them as ` - `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_AMOUNT`: either `Amount` or `Amount2` is zero, negative, or has invalid mantissa +- `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L73-L227) +[^ammcreate-preclaim-validation]: Validation against ledger view (preclaim): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L95-L240) - `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`: either asset is globally or individually frozen +- `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 @@ -616,11 +614,11 @@ The two amounts can be in any order - the AMM will automatically order them as ` - `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 [`checkMPTTxAllowed`](../mpts/README.md#362-checkmptxallowed). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. +- 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L242-L335) +[^ammcreate-doapply-validation]: Validation during doApply: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251-L329) - `tecDUPLICATE`: - AMM pseudo-account ID generation failed (no valid account ID found after 256 attempts) @@ -630,7 +628,7 @@ The two amounts can be in any order - the AMM will automatically order them as ` ### 3.1.2. State Changes[^ammcreate-state-changes] -[^ammcreate-state-changes]: State changes (doApply): [`AMMCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMCreate.cpp#L242-L383) +[^ammcreate-state-changes]: State changes (doApply): [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L251-L372) - `AccountRoot` object is **created** for AMM pseudo-account: - `Account`: Generated pseudo-account ID (from collision-avoidance algorithm) @@ -668,7 +666,7 @@ The two amounts can be in any order - the AMM will automatically order them as ` - For each MPT asset: MPToken entry for the AMM pseudo-account - Flags: - `lsfMPTAMM`: Marks this as an AMM-owned MPToken entry - - `lsfMPTAuthorized`: Set if the MPTokenIssuance has `lsfMPTRequireAuth` flag + - `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 @@ -721,7 +719,7 @@ The deposit mode is determined by exactly one of these flags (enforced by checki **Static validation**[^ammdeposit-static-validation] -[^ammdeposit-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L15-L30), [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L34-L38), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L41-L163) +[^ammdeposit-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L37-L47), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L51-L54), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L57-L175) - `temDISABLED`: - AMM amendment is not enabled @@ -735,27 +733,27 @@ The deposit mode is determined by exactly one of these flags (enforced by checki - `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 -- `temBAD_CURRENCY`: `Amount`, `Amount2`, or `EPrice` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) -- `temBAD_ISSUER`: `Amount`, `Amount2`, or `EPrice` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_AMOUNT`: `Amount`, `Amount2`, or `EPrice` is zero, negative, or has invalid mantissa + - `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L166-L380) +[^ammdeposit-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L178-L380) - `terNO_AMM`: AMM ledger entry does not exist for specified asset pair - `tecINTERNAL`: - - AMM account does not exist or pool balances cannot be retrieved (should not happen if AMM ledger entry exists) - - (tfTwoAssetIfEmpty only) Pool has LP tokens but asset balances are not zero + - (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) -- With [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback): - - `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`: `Asset` or `Asset2` is frozen (AMM account, currency, or depositor account is frozen) +- 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) - `tecUNFUNDED_AMM`: - account has insufficient token balance to deposit - account has insufficient XRP to deposit (and LP token trust line already exists) @@ -763,11 +761,11 @@ The deposit mode is determined by exactly one of these flags (enforced by checki - 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 [`checkMPTTxAllowed`](../mpts/README.md#362-checkmptxallowed). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. +- 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L499-L1047) +[^ammdeposit-doapply-validation]: Validation during doApply: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L383-L1014) - `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). @@ -845,7 +843,7 @@ The withdrawal mode is determined by exactly one of these flags (enforced by che **Static validation**[^ammwithdraw-static-validation] -[^ammwithdraw-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L15-L30), [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L34-L37), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L40-L151) +[^ammwithdraw-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L43-L53), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L57-L60), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L63-L173) - `temDISABLED`: - AMM amendment not enabled @@ -859,9 +857,10 @@ The withdrawal mode is determined by exactly one of these flags (enforced by che - `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`: `Amount`, `Amount2`, or `EPrice` uses the disallowed 3-letter "XRP" currency code (`0x5852500000000000`) -- `temBAD_ISSUER`: `Amount`, `Amount2`, or `EPrice` is XRP (currency is all zeros) but has a non-zero issuer account -- `temBAD_AMOUNT`: `Amount`, `Amount2`, or `EPrice` is zero, negative, or has invalid mantissa +- `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: @@ -871,29 +870,27 @@ The withdrawal mode is determined by exactly one of these flags (enforced by che **Validation against the ledger view**[^ammwithdraw-preclaim-validation] -[^ammwithdraw-preclaim-validation]: Validation against ledger view (preclaim): [`AMMWithdraw.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L165-L295) +[^ammwithdraw-preclaim-validation]: Validation against ledger view (preclaim): [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L187-L304) - `terNO_AMM`: AMM ledger entry does not exist for specified asset pair - `tecINTERNAL`: - - AMM account does not exist or pool balances cannot be retrieved (should not happen if AMM ledger entry exists) - 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 -- With [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback): - - `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`: `Asset` or `Asset2` is frozen (AMM account, currency, or withdrawer account is frozen) +- `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) - `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 [`checkMPTTxAllowed`](../mpts/README.md#362-checkmptxallowed). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for validation logic and error conditions. +- 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L298-L421) +[^ammwithdraw-doapply-validation]: Validation during doApply: [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L307-L422) - 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) @@ -904,7 +901,7 @@ The withdrawal mode is determined by exactly one of these flags (enforced by che - `tecAMM_FAILED`: Withdrawal constraints not satisfied (calculated withdrawal amounts don't meet minimum requirements specified in transaction fields) - `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 `maxDeletableAMMTrustLines` trust lines per transaction. +- `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 @@ -915,7 +912,7 @@ The withdrawal mode is determined by exactly one of these flags (enforced by che - `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 `maxDeletableAMMTrustLines` per transaction) + - 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. @@ -932,10 +929,6 @@ The withdrawal mode is determined by exactly one of these flags (enforced by che - `OwnerCount`: Decremented if LP token trust line deleted - `OwnerCount`: Incremented if new trust line created for withdrawn token (with fixAMMv1_2) -- Vote slots may be **updated**: - - Recalculated proportionally if LP token balances changed - - Votes from accounts with zero LP tokens removed - ## 3.4. AMMVote Transaction The `AMMVote` transaction allows LP token holders to vote on the AMM's trading fee. @@ -955,7 +948,7 @@ The `AMMVote` transaction allows LP token holders to vote on the AMM's trading f **Static validation**[^ammvote-static-validation] -[^ammvote-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMVote.cpp#L12-L23), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMVote.cpp#L26-L41) +[^ammvote-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L32-L39), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L42-L57) - `temDISABLED`: - AMM amendment not enabled @@ -968,7 +961,7 @@ The `AMMVote` transaction allows LP token holders to vote on the AMM's trading f **Validation against the ledger view**[^ammvote-preclaim-validation] -[^ammvote-preclaim-validation]: Validation against ledger view (preclaim): [`AMMVote.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMVote.cpp#L44-L64) +[^ammvote-preclaim-validation]: Validation against ledger view (preclaim): [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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 @@ -976,7 +969,7 @@ The `AMMVote` transaction allows LP token holders to vote on the AMM's trading f **Validation during doApply**[^ammvote-doapply-validation] -[^ammvote-doapply-validation]: Validation during doApply: [`AMMVote.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMVote.cpp#L66-L218) +[^ammvote-doapply-validation]: Validation during doApply: [`AMMVote.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMVote.cpp#L82-L235) - `tecINTERNAL`: AMM ledger entry does not exist (should not happen if preclaim succeeded) @@ -1023,7 +1016,7 @@ See [Bidding documentation](bidding.md) for more details. **Static validation**[^ammbid-static-validation] -[^ammbid-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMBid.cpp#L15-L26), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMBid.cpp#L29-L81) +[^ammbid-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L38-L48), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L51-L103) - `temDISABLED`: - AMM amendment not enabled @@ -1039,7 +1032,7 @@ See [Bidding documentation](bidding.md) for more details. **Validation against the ledger view**[^ammbid-preclaim-validation] -[^ammbid-preclaim-validation]: Validation against ledger view (preclaim): [`AMMBid.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMBid.cpp#L84-L157) +[^ammbid-preclaim-validation]: Validation against ledger view (preclaim): [`AMMBid.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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 @@ -1047,12 +1040,12 @@ See [Bidding documentation](bidding.md) for more details. - `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` exceeds account's LP token holdings or AMM's total LP token balance + - `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMBid.cpp#L160-L354) +[^ammbid-doapply-validation]: Validation during doApply: [`AMMBid.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMBid.cpp#L180-L363) - `tecAMM_FAILED`: Computed price exceeds `BidMax` - `tecAMM_INVALID_TOKENS`: Pay price exceeds LP token holdings @@ -1067,7 +1060,7 @@ The AMMBid transaction executes through the `applyBid()` function, which determi - `Account`: Set to bidder - `Expiration`: Set to current time + 86,400 seconds - `Price`: Set to amount paid - - `DiscountedFee`: Set to `TradingFee / 10` + - `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 @@ -1097,7 +1090,7 @@ The `AMMDelete` transaction is used to clean up AMM instances that have been emp **Static validation**[^ammdelete-static-validation] -[^ammdelete-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDelete.cpp#L12-L23), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDelete.cpp#L26-L29) +[^ammdelete-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L23-L30), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDelete.cpp#L33-L36) - `temDISABLED`: - AMM amendment not enabled @@ -1106,14 +1099,14 @@ The `AMMDelete` transaction is used to clean up AMM instances that have been emp **Validation against the ledger view**[^ammdelete-preclaim-validation] -[^ammdelete-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDelete.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDelete.cpp#L32-L47) +[^ammdelete-preclaim-validation]: Validation against ledger view (preclaim): [`AMMDelete.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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: [`AMMUtils.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMUtils.cpp#L327-L388) +[^ammdelete-doapply-validation]: Validation during doApply: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L713-L766) - `tecINTERNAL`: - AMM ledger entry does not exist (should not happen if preclaim succeeded) @@ -1123,7 +1116,7 @@ The `AMMDelete` transaction is used to clean up AMM instances that have been emp - 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 `maxDeletableAMMTrustLines`). The transaction should be called again to continue deletion. This is not an error - it indicates partial success. +- `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 @@ -1138,16 +1131,15 @@ The AMMDelete transaction cleans up an empty AMM instance. The deletion process - 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 - - Both sides of the trust line have their `OwnerCount` decremented + - 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 `maxDeletableAMMTrustLines` trust lines per transaction + - 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) - - The issuer's `OwnerCount` is decremented for each deleted MPToken - - Directory entries are removed from both the AMM pseudo-account's and issuer's owner directories + - 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**: @@ -1168,7 +1160,7 @@ The AMMDelete transaction cleans up an empty AMM instance. The deletion process When there are too many trust lines to delete in a single transaction: - `RippleState` objects are **partially deleted**: - - Up to `maxDeletableAMMTrustLines` trust lines are 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` @@ -1187,11 +1179,11 @@ When there are too many trust lines to delete in a single transaction: - Owner directory still contains remaining trust lines and MPTokens (if present) - **Subsequent AMMDelete transactions** must be submitted: - - Each transaction deletes up to `maxDeletableAMMTrustLines` more trust lines + - 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 `maxDeletableAMMTrustLines` 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. +**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) @@ -1200,13 +1192,13 @@ The deletion process: 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 `maxDeletableAMMTrustLines`), the transaction returns `tecINCOMPLETE` and must be called again. +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#321-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. +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:** @@ -1216,7 +1208,7 @@ When no `Amount` is provided, the transaction burns all of the holder's LP token 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 (`fhIGNORE_FREEZE` and `ahIGNORE_AUTH`), 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. +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:** @@ -1249,7 +1241,7 @@ The transaction uses AMM withdrawal logic internally: **Static validation**[^ammclawback-static-validation] -[^ammclawback-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMClawback.cpp#L24-L38), [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMClawback.cpp#L18-L21), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMClawback.cpp#L41-L88) +[^ammclawback-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L43-L53), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L37-L40), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L56-L99) - `temDISABLED`: - [AMMClawback](https://xrpl.org/resources/known-amendments#ammclawback) amendment not enabled @@ -1267,7 +1259,7 @@ The transaction uses AMM withdrawal logic internally: **Validation against the ledger view**[^ammclawback-preclaim-validation] -[^ammclawback-preclaim-validation]: Validation against ledger view (preclaim): [`AMMClawback.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMClawback.cpp#L91-L149) +[^ammclawback-preclaim-validation]: Validation against ledger view (preclaim): [`AMMClawback.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L102-L154) - `terNO_ACCOUNT`: Issuer account or holder account does not exist - `terNO_AMM`: AMM pool does not exist for the specified asset pair @@ -1282,7 +1274,7 @@ The transaction uses AMM withdrawal logic internally: **Validation during doApply**[^ammclawback-doapply-validation] -[^ammclawback-doapply-validation]: Validation during doApply: [`AMMClawback.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMClawback.cpp#L164-L284) +[^ammclawback-doapply-validation]: Validation during doApply: [`AMMClawback.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMClawback.cpp#L169-L291) - `tecINTERNAL`: - AMM ledger entry does not exist @@ -1328,7 +1320,7 @@ The `AMMClawback` transaction withdraws assets from an AMM pool by burning LP to **Asset Distribution:** - **`Asset` (always clawed back)**: - - For trust line tokens: Transferred from holder to issuer via `rippleCredit`, adjusting the shared `RippleState` balance + - 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)**: diff --git a/docs/amms/bidding.md b/docs/amms/bidding.md index 1930087..b29dc59 100644 --- a/docs/amms/bidding.md +++ b/docs/amms/bidding.md @@ -30,7 +30,7 @@ The auction slot solves this by offering discounted trading fees to the slot own 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 only 6 hours (25% of the 24-hour period), they receive a refund of 750 LP tokens (75% 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. +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. @@ -84,12 +84,12 @@ def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): return (tecINTERNAL, false) auctionSlot = ammSle.peekFieldObject(sfAuctionSlot) - current = ctx.view().info().parentCloseTime # in seconds + current = ctx.view().header().parentCloseTime # in seconds # Calculate fees and prices - discountedFee = ammSle[sfTradingFee] / AUCTION_SLOT_DISCOUNTED_FEE_FRACTION # Divide by 10 + discountedFee = ammSle[sfTradingFee] / kAuctionSlotDiscountedFeeFraction # Divide by 10 tradingFee = getFee(ammSle[sfTradingFee]) - minSlotPrice = lptAMMBalance * tradingFee / AUCTION_SLOT_MIN_FEE_FRACTION # Divide by 25 + minSlotPrice = lptAMMBalance * tradingFee / kAuctionSlotMinFeeFraction # Divide by 25 # Determine current time slot (0-19) # Returns None if slot is not owned or expired @@ -117,7 +117,7 @@ def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): discountedFee, payPrice, payPrice, # burn entire amount (no refund) - lpTokens.issue(), + lpTokens.asset(), lptAMMBalance, ctx.tx, ) @@ -125,7 +125,7 @@ def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): # CASE 2: Slot is currently owned pricePurchased = auctionSlot[sfPrice] - fractionUsed = (timeSlot + 1) / AUCTION_SLOT_TIME_INTERVALS # timeSlot is 0-19, so (timeSlot+1)/20 + fractionUsed = (timeSlot + 1) / kAuctionSlotTimeIntervals # timeSlot is 0-19, so (timeSlot+1)/20 fractionRemaining = 1 - fractionUsed # Calculate computed price based on time slot @@ -152,7 +152,7 @@ def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): sb, from = account, # New bidder pays to = auctionSlot[sfAccount], # Previous owner receives - amount = toSTAmount(lpTokens.issue(), refund), + amount = toSTAmount(lpTokens.asset(), refund), ) if result != tesSUCCESS: log "AMM Bid: failed to refund" @@ -169,7 +169,7 @@ def applyBid(ctx: ApplyContext, sb: &Sandbox, account: AccountID): discountedFee, payPrice, burn, - lpTokens.issue(), + lpTokens.asset(), lptAMMBalance, ctx.tx, ) @@ -203,11 +203,11 @@ def updateSlot( # Update auction slot fields auctionSlot.setAccountID(sfAccount, account) - auctionSlot.setFieldU32(sfExpiration, current + TOTAL_TIME_SLOT_SECS) # +86,400 seconds + auctionSlot.setFieldU32(sfExpiration, current + kTotalTimeSlotSecs) # +86,400 seconds if fee != 0: auctionSlot.setFieldU16(sfDiscountedFee, fee) - else: + elif auctionSlot.isFieldPresent(sfDiscountedFee): auctionSlot.makeFieldAbsent(sfDiscountedFee) auctionSlot.setFieldAmount(sfPrice, toSTAmount(lpTokenIssue, price)) @@ -218,7 +218,7 @@ def updateSlot( auctionSlot.makeFieldAbsent(sfAuthAccounts) # Burn LP tokens - saBurn = adjustLPTokens(lptAMMBalance, toSTAmount(lpTokenIssue, burn), IsDeposit::No) # helpers.md#12-adjustlptokens + saBurn = adjustLPTokens(lptAMMBalance, toSTAmount(lpTokenIssue, burn), IsDeposit::No) # helpers.md#22-adjustlptokens if saBurn >= lptAMMBalance: # This should never happen @@ -356,8 +356,8 @@ The `0.3^60` is essentially 0, so the price is close to the full 105% markup ear The auction slot is divided into 20 time intervals over 24 hours: ``` -TOTAL_TIME_SLOT_SECS = 86,400 seconds (24 hours) -AUCTION_SLOT_TIME_INTERVALS = 20 +kTotalTimeSlotSecs = 86,400 seconds (24 hours) +kAuctionSlotTimeIntervals = 20 Each interval = 86,400 / 20 = 4,320 seconds (~1.2 hours) ``` @@ -429,7 +429,7 @@ The burn amount (bid price minus refund) is removed from the LP token supply: 3. **Redeem (burn) LP tokens** from bidder's balance: ``` - redeemIOU(sb, account, saBurn, lpTokens.issue()) + redeemIOU(sb, account, saBurn, lpTokens.get()) ``` 4. **Decrease AMM's LPTokenBalance**: diff --git a/docs/amms/deposit.md b/docs/amms/deposit.md index 5eeb00a..b0ec4f1 100644 --- a/docs/amms/deposit.md +++ b/docs/amms/deposit.md @@ -39,7 +39,7 @@ The `applyGuts` function[^apply-guts] is the main entry point for processing AMM It retrieves the AMM ledger entry and current pool balances, then determines which trading fee applies to the depositor (regular or discounted for [auction slot holders](#3-gettradingfee)). Based on the transaction flags and provided fields, it dispatches to one of deposit mode handlers: three [multi-asset modes](#4-multi-asset-deposit-modes) that maintain proportional deposits or reinitialize empty pools, and three [single-asset modes](#5-single-asset-deposit-modes) that perform single-sided deposits. Each mode handler calculates the deposit amounts and LP tokens to issue, then calls the [common deposit function](#6-common-deposit-function) to execute the actual asset transfers and update the pool state. -[^apply-guts]: `AMMDeposit::applyGuts`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L383-L496) +[^apply-guts]: `AMMDeposit::applyGuts`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L383-L484) ## 2.1. applyGuts Pseudo-Code @@ -50,14 +50,14 @@ def applyGuts(sb: &Sandbox, tx: Transaction): ePrice = tx[sfEPrice] ammSle = sb.getAMM(amount, amount2) if not ammSle: - return tecInternal + return tecINTERNAL ammAccountId = ammSle[sfAccount] # In `rippled`, this is called "expected", but probably only to reflect the returned type. A better name is "existing" or "currentBalances" - # ZERO_IF_FROZEN: treat frozen assets as having zero balance - # ZERO_IF_UNAUTHORIZED: treat unauthorized MPT holders as having zero balance - currentBalances = ammHolds(sb, ammSle, amount, amount2, ZERO_IF_FROZEN, ZERO_IF_UNAUTHORIZED) + # FreezeHandling::ZeroIfFrozen: treat frozen assets as having zero balance + # AuthHandling::ZeroIfUnauthorized: treat unauthorized MPT holders as having zero balance + currentBalances = ammHolds(sb, ammSle, amount, amount2, FreezeHandling::ZeroIfFrozen, AuthHandling::ZeroIfUnauthorized) if not currentBalances: return currentBalances.error() @@ -74,7 +74,7 @@ def applyGuts(sb: &Sandbox, tx: Transaction): # Normal pool: get current trading fee (with potential discount) # Returns discounted fee if account is auction slot holder or authorized # Otherwise returns regular AMM trading fee - tfee = getTradingFee(view, ammSle, account_) + tfee = getTradingFee(view, ammSle, accountID_) subTxType = tx.getFlags() & tfDepositSubTx @@ -151,7 +151,7 @@ def applyGuts(sb: &Sandbox, tx: Transaction): ammAccountID, amount, # amount1 to deposit amount2, # amount2 to deposit - lptAMMBalance.issue, + lptAMMBalance.asset(), tfee ) @@ -171,8 +171,8 @@ def applyGuts(sb: &Sandbox, tx: Transaction): initializeFeeAuctionVote( sb, ammSle, - account_, - lptAMMBalance.issue, + accountID_, + lptAMMBalance.asset(), tfee ) @@ -188,7 +188,7 @@ The `getTradingFee` function[^get-trading-fee] is called by [`applyGuts`](#2-app It checks if the depositor holds the [auction slot](README.md#121-auction-slot) or is listed in the slot's authorized accounts and if the auction slot has not expired. If so, it returns the discounted fee (1/10th of the regular fee). Otherwise, it returns the AMM's standard trading fee. -[^get-trading-fee]: `getTradingFee`: [`AMMUtils.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMUtils.cpp#L163-L192) +[^get-trading-fee]: `getTradingFee`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L566-L593) ## 3.1. getTradingFee Pseudo-Code @@ -236,7 +236,7 @@ The three modes are: The `equalDepositLimit` function[^equal-deposit-limit] handles proportional deposits where the depositor specifies maximum amounts they're willing to provide for both assets (`Amount` and `Amount2`). Since the deposit must maintain the pool's ratio, the function cannot simply use both maximum amounts - one will typically be limiting while the other has excess. -[^equal-deposit-limit]: `AMMDeposit::equalDepositLimit`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L738-L804) +[^equal-deposit-limit]: `AMMDeposit::equalDepositLimit`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L710-L780) The function tries two strategies to maximize the deposit within the user's constraints. First, it attempts to use 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 = tokensAdj / lptAMMBalance`) to ensure precision consistency. Using this adjusted fraction, it calculates the proportional amount2 needed. If this amount2 fits within `Amount2`, the deposit proceeds immediately. @@ -343,9 +343,9 @@ def equalDepositLimit( Proportional deposit for exact LP tokens.[^equal-deposit-tokens] -[^equal-deposit-tokens]: `AMMDeposit::equalDepositTokens`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L662-L707) +[^equal-deposit-tokens]: `AMMDeposit::equalDepositTokens`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L637-L679) -This function handles the reverse calculation from [`equalDepositLimit`](#41-equaldepositlimit-tfTwoAsset): the user specifies the exact number of LP tokens they want to receive, and the function calculates the required proportional amounts of both assets. The user provides `LPTokenOut` (exact LP tokens desired) and optionally `Amount` and `Amount2` as minimum constraints on the deposit amounts. The function first adjusts the requested LP tokens for precision using [`adjustLPTokensOut`](helpers.md#24-adjustlptokensout-deposits), then calculates the pool fraction these tokens represent (`frac = tokensAdj / lptAMMBalance`). Using this fraction, it calculates the required amounts of both assets by multiplying each pool balance by the fraction, rounding up with [`getRoundedAsset`](helpers.md#23-getroundedasset) to ensure the pool receives sufficient assets. If the calculated deposit amounts are less than the optional `Amount` and `Amount2` minimums, the transaction fails with `tecAMM_FAILED`. +This function handles the reverse calculation from [`equalDepositLimit`](#41-equaldepositlimit-tftwoasset): the user specifies the exact number of LP tokens they want to receive, and the function calculates the required proportional amounts of both assets. The user provides `LPTokenOut` (exact LP tokens desired) and optionally `Amount` and `Amount2` as minimum constraints on the deposit amounts. The function first adjusts the requested LP tokens for precision using [`adjustLPTokensOut`](helpers.md#24-adjustlptokensout-deposits), then calculates the pool fraction these tokens represent (`frac = tokensAdj / lptAMMBalance`). Using this fraction, it calculates the required amounts of both assets by multiplying each pool balance by the fraction, rounding up with [`getRoundedAsset`](helpers.md#23-getroundedasset) to ensure the pool receives sufficient assets. If the calculated deposit amounts are less than the optional `Amount` and `Amount2` minimums, the transaction fails with `tecAMM_FAILED`. **Example:** @@ -405,7 +405,7 @@ def equalDepositTokens( Reinitialize an empty AMM pool.[^equal-deposit-in-empty-state] -[^equal-deposit-in-empty-state]: `AMMDeposit::equalDepositInEmptyState`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L1025-L1045) +[^equal-deposit-in-empty-state]: `AMMDeposit::equalDepositInEmptyState`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L994-L1014) This is a special mode that handles the case where all LP tokens have been withdrawn from an AMM, which means both asset balances are also zero (enforced by the [withdrawal system](README.md#13-ammwithdraw)). The depositor specifies both `Amount` and `Amount2` to set a new pool ratio, similar to [`AMMCreate`](README.md#11-ammcreate). The function calculates initial LP tokens using the geometric mean formula (`sqrt(amount * amount2)`), the same formula used when creating a new AMM. The depositor can optionally provide `TradingFee` to set a new fee for the pool. Unlike other deposit modes, there are no minimum constraints since the depositor is establishing the initial terms for the reinitialized pool. @@ -455,7 +455,7 @@ Single-asset deposits allow users to deposit only one asset instead of both asse Deposit a single asset to receive calculated LP tokens.[^single-deposit] -[^single-deposit]: `AMMDeposit::singleDeposit`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L815-L852) +[^single-deposit]: `AMMDeposit::singleDeposit`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L791-L828) The user specifies `Amount` (the asset to deposit) and the function calculates how many LP tokens they receive. Since this is a single-asset deposit that changes the pool ratio, a [trading fee](#3-gettradingfee) applies. The function uses [`lpTokensOut`](helpers.md#321-lptokensout-equation-3) to calculate the LP tokens based on the deposit amount and trading fee, then adjusts the result for precision with [`adjustLPTokensOut`](helpers.md#24-adjustlptokensout-deposits). The adjusted tokens are passed to [`adjustAssetInByTokens`](helpers.md#26-adjustassetinbytokens) to recalculate the deposit amount, ensuring the reverse calculation doesn't exceed the user's specified amount due to rounding. The optional `LPTokenOut` field provides a minimum constraint - if the calculated LP tokens are less than this value, the transaction fails with `tecAMM_FAILED`. @@ -508,7 +508,7 @@ def singleDeposit( Deposit a single asset to receive exact LP tokens.[^single-deposit-tokens] -[^single-deposit-tokens]: `AMMDeposit::singleDepositTokens`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L862-L892) +[^single-deposit-tokens]: `AMMDeposit::singleDepositTokens`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L838-L866) This function handles the reverse calculation from [`singleDeposit`](#51-singledeposit-tfsingleasset): the user specifies the exact number of LP tokens they want to receive using `LPTokenOut`, and the function calculates the required deposit amount of a single asset. The user provides `Amount` as a maximum constraint on how much they're willing to deposit. The function first adjusts the requested LP tokens for precision using [`adjustLPTokensOut`](helpers.md#24-adjustlptokensout-deposits), then uses [`ammAssetIn`](helpers.md#322-ammassetin-equation-4) (Equation 4) to calculate the required deposit amount by solving the inverse single-asset deposit problem. If the calculated amount exceeds the user's `Amount` constraint, the transaction fails with `tecAMM_FAILED`. @@ -559,9 +559,9 @@ def singleDepositTokens( Deposit a single asset with an effective price limit.[^single-deposit-eprice] -[^single-deposit-eprice]: `AMMDeposit::singleDepositEPrice`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L920-L1022) +[^single-deposit-eprice]: `AMMDeposit::singleDepositEPrice`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L894-L991) -This mode allows users to control the maximum [effective price](README.md#121-effective-price) they're willing to pay per LP token (effective price = asset deposited / LP tokens received). The user provides `EPrice` (maximum effective price) and `Amount` (deposit amount, or zero). +This mode allows users to control the maximum [effective price](README.md#114-effective-price) they're willing to pay per LP token (effective price = asset deposited / LP tokens received). The user provides `EPrice` (maximum effective price) and `Amount` (deposit amount, or zero). There are two scenarios: If `Amount` is non-zero, the function calculates the LP tokens using [`lpTokensOut`](helpers.md#321-lptokensout-equation-3), adjusts for precision, and checks if the resulting effective price is within the limit - if acceptable, the deposit proceeds; otherwise it falls through to scenario 2. @@ -706,7 +706,7 @@ def singleDepositEPrice( The `deposit()` function[^deposit] is called by all deposit modes to perform the actual asset transfers. This function validates minimum constraints for slippage protection, checks the depositor has sufficient funds for the deposit, transfers the assets from the depositor to the AMM account, and issues LP tokens to the depositor (creating a trust line if needed). -[^deposit]: AMMDeposit::deposit: [AMMDeposit.cpp:513-645](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L513-L645) +[^deposit]: `AMMDeposit::deposit`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L501-L620) ## 6.1. deposit Pseudo-Code diff --git a/docs/amms/helpers.md b/docs/amms/helpers.md index 7e9dfe0..01a6f2e 100644 --- a/docs/amms/helpers.md +++ b/docs/amms/helpers.md @@ -60,8 +60,8 @@ There are two overloaded versions: 1. **Simple version**: Takes a direct fraction value[^get-rounded-lp-tokens-simple] - used by [`equalDepositLimit`](deposit.md#411-equaldepositlimit-pseudo-code), [`equalWithdrawLimit`](withdraw.md#421-equalwithdrawlimit-pseudo-code) 2. **Callback version**: Takes lambda callbacks for delayed evaluation[^get-rounded-lp-tokens-callback] - used by [`singleDepositEPrice`](deposit.md#531-singledepositeprice-pseudo-code), [`singleWithdrawEPrice`](withdraw.md#531-singlewithdraweprice-pseudo-code) -[^get-rounded-lp-tokens-simple]: Simple version of `getRoundedLPTokens`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L292-L304) -[^get-rounded-lp-tokens-callback]: Callback version of `getRoundedLPTokens`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L307-L327) +[^get-rounded-lp-tokens-simple]: Simple version of `getRoundedLPTokens`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L319-L331) +[^get-rounded-lp-tokens-callback]: Callback version of `getRoundedLPTokens`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L334-L354) The callback version exists to avoid redundant calculations when the input depends on complex intermediate calculations. The lambdas delay evaluation so that: - If `fixAMMv1_3` is disabled, only `noRoundCb()` is called @@ -70,8 +70,8 @@ This prevents computing expensive formulas twice. For simple proportional operat The `Number` class uses thread-local global state for its rounding mode[^number-thread-local-mode]. When `NumberRoundModeGuard` sets the rounding mode, all subsequent `Number` arithmetic operations automatically use that mode. The callbacks perform operations like `amountDeposit / ePrice` where both operands are `STAmount` values that implicitly convert to `Number`[^stamount-to-number-conversion], so the division respects the global rounding mode without needing to pass the mode as a parameter. -[^number-thread-local-mode]: Thread-local rounding mode in `Number` class: [`Number.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/basics/Number.h#L185) -[^stamount-to-number-conversion]: `STAmount` to `Number` conversion operator: [`STAmount.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STAmount.h#L500-L509) +[^number-thread-local-mode]: Thread-local rounding mode in `Number` class: [`Number.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/basics/Number.h#L538) +[^stamount-to-number-conversion]: `STAmount` to `Number` conversion operator: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STAmount.h#L524-L533) ### 2.1.1. getRoundedLPTokens (Simple) Pseudo-Code @@ -123,7 +123,7 @@ def getRoundedLPTokens( Adjust LP tokens to account for precision loss when updating the balance.[^adjust-lp-tokens] -[^adjust-lp-tokens]: `adjustLPTokens`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L154-L165) +[^adjust-lp-tokens]: `adjustLPTokens`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L182-L190) This is a low-level helper function that handles a precision issue. When adding or removing a small amount to a very large pool balance, precision is lost when using `Number` - the resulting balance may be less than the actual sum would be with infinite precision. @@ -151,8 +151,8 @@ There are two overloaded versions: 1. **Simple version**: Takes a direct fraction value[^get-rounded-asset-simple] - used for proportional deposits/withdrawals 2. **Callback version**: Takes lambda callbacks for delayed evaluation[^get-rounded-asset-callback] - used for complex single-asset calculations -[^get-rounded-asset-simple]: Simple version of `getRoundedAsset`: [`AMMHelpers.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/AMMHelpers.h#L658-L673) -[^get-rounded-asset-callback]: Callback version of `getRoundedAsset`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L274-L289) +[^get-rounded-asset-simple]: Simple version of `getRoundedAsset`: [`AMMHelpers.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/helpers/AMMHelpers.h#L623-L638) +[^get-rounded-asset-callback]: Callback version of `getRoundedAsset`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L301-L316) Please refer to [`getRoundedLPTokens`](#21-getroundedlptokens) for the reasoning and the explanation behind the callback version. @@ -202,7 +202,7 @@ def getRoundedAsset( Adjust LP tokens for deposits (outgoing from AMM).[^adjust-lp-tokens-out] -[^adjust-lp-tokens-out]: `adjustLPTokensOut`: [`AMMDeposit.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMDeposit.cpp#L648-L656) +[^adjust-lp-tokens-out]: `adjustLPTokensOut`: [`AMMDeposit.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMDeposit.cpp#L623-L631) This wrapper function applies precision adjustments specifically for deposit operations. It calls [`adjustLPTokens`](#22-adjustlptokens) with `isDeposit=True`, ensuring downward rounding that slightly reduces the LP tokens issued to depositors. @@ -221,7 +221,7 @@ def adjustLPTokensOut(rules, lptAMMBalance, lpTokensDeposit): Adjust LP tokens for withdrawals (incoming to AMM).[^adjust-lp-tokens-in] -[^adjust-lp-tokens-in]: `adjustLPTokensIn`: [`AMMWithdraw.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L725-L734) +[^adjust-lp-tokens-in]: `adjustLPTokensIn`: [`AMMWithdraw.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L712-L721) This wrapper function applies precision adjustments specifically for withdrawal operations. It calls [`adjustLPTokens`](#22-adjustlptokens) with `isDeposit=False`. @@ -240,7 +240,7 @@ def adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, withdrawAll): Adjust asset deposit amount and LP tokens to handle rounding edge cases.[^adjust-asset-in-by-tokens] -[^adjust-asset-in-by-tokens]: `adjustAssetInByTokens`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L330-L353) +[^adjust-asset-in-by-tokens]: `adjustAssetInByTokens`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L357-L380) This function is used by single-asset deposit operations to handle a precision edge case. When a user specifies a deposit amount, the system calculates LP tokens using [`lpTokensOut`](#321-lptokensout-equation-3) (Equation 3), then adjusts those tokens with [`adjustLPTokens`](#22-adjustlptokens). However, when we reverse the calculation using [`ammAssetIn`](#322-ammassetin-equation-4) (Equation 4) to verify the deposit amount, rounding can cause the calculated amount to exceed the user's original deposit amount. @@ -291,7 +291,7 @@ Swap formulas are used when trading assets through the AMM pool. Calculate output amount given a specific input amount.[^swap-asset-in] It answers the question: "If I provide X amount of input asset, how much output asset do I receive?" -[^swap-asset-in]: `swapAssetIn`: [`AMMHelpers.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/AMMHelpers.h#L444-L504) +[^swap-asset-in]: `swapAssetIn`: [`AMMHelpers.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/helpers/AMMHelpers.h#L424-L477) This function uses the formula `out = B - (A * B) / (A + in * (1 - fee))`, but with rounding at every step to protect the AMM pool. Used by [`BookStep`](../flow/steps.md#54-amm-integration) in the payment engine when consuming AMM liquidity. The function rounds downward, ensuring the pool gives out slightly less than the theoretical amount to protect against precision-based value leakage. @@ -330,7 +330,7 @@ def swapAssetIn( Calculate required input amount for a specific desired output.[^swap-asset-out] It answers the question: "If I want to receive Y amount of output asset, how much input asset must I provide?" -[^swap-asset-out]: `swapAssetOut`: [`AMMHelpers.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/AMMHelpers.h#L517-L577) +[^swap-asset-out]: `swapAssetOut`: [`AMMHelpers.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/helpers/AMMHelpers.h#L490-L543) This is the inverse of [`swapAssetIn`](#311-swapassetin). It uses the formula `in = ((A * B) / (B - out) - A) / (1 - fee)` to determine how much input is needed to extract a specific output amount while maintaining the pool ratio. Used by [`BookStep`](../flow/steps.md#54-amm-integration) when the payment engine needs to deliver a specific amount. @@ -396,18 +396,18 @@ As `out` **increases**, the denominator `(B-out)` **decreases**, making quality In `rippled`, this can be verified using `rippled --unittest=AMMCalc` and appropriate logging, e.g.: ```bash -rippled --unittest=AMMCalc --unittest-arg "swapout,A(USD(1000),EUR(10000)),EUR(5000),300 +rippled --unittest=AMMCalc --unittest-arg "swapout,A(USD(1000),EUR(10000)),EUR(5000),300" ``` ### 3.1.4. changeSpotPriceQuality Calculate an AMM offer size such that after the swap, the pool's reserve ratio equals a target quality (typically the best CLOB offer quality).[^change-spot-price-quality] -[^change-spot-price-quality]: `changeSpotPriceQuality`: [`AMMHelpers.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/AMMHelpers.h#L311-L419) +[^change-spot-price-quality]: `changeSpotPriceQuality`: [`AMMHelpers.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/helpers/AMMHelpers.h#L302-L399) This function is called from `AMMLiquidity::getOffer()` during single-path pathfinding when integrating AMM and CLOB (Central Limit Order Book) liquidity.[^amm-liquidity-get-offer] When a CLOB quality is available for comparison, this function sizes the AMM offer so that consuming it would move the AMM's spot price to match the best CLOB offer quality. -[^amm-liquidity-get-offer]: AMMLiquidity getOffer usage: [`AMMLiquidity.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMLiquidity.cpp#L193-L194) +[^amm-liquidity-get-offer]: AMMLiquidity getOffer usage: [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L211-L212) When one side of the pool is XRP, the algorithm calculates the XRP side first (whether that's takerGets or takerPays), then derives the other side from it. This differs from IOU-only or MPT pools where takerPays is calculated first. @@ -424,7 +424,7 @@ def changeSpotPriceQuality( This pseudocode assumes fixAMMv1_1 is enabled """ - if isXRP(getIssue(pool.out)): + if isXRP(getAsset(pool.out)): # TakerGets is XRP - calculate it first return getAMMOfferStartWithTakerGets(pool, quality, tfee) else: @@ -436,7 +436,7 @@ def changeSpotPriceQuality( Calculate AMM offer starting with takerGets (output) when takerGets is XRP.[^get-amm-offer-start-with-taker-gets] -[^get-amm-offer-start-with-taker-gets]: `getAMMOfferStartWithTakerGets`: [`AMMHelpers.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/AMMHelpers.h#L176-L220) +[^get-amm-offer-start-with-taker-gets]: `getAMMOfferStartWithTakerGets`: [`AMMHelpers.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/helpers/AMMHelpers.h#L175-L215) **Two scenarios considered:** @@ -484,7 +484,7 @@ def getAMMOfferStartWithTakerGets(pool, targetQuality, tfee): # Round takerGets downward (minimizes offer, maximizes quality) # This has most impact when takerGets is XRP - takerGets = toAmount(getIssue(pool.out), takerGets, roundingMode=downward) + takerGets = toAmount(getAsset(pool.out), takerGets, roundingMode=downward) # Calculate takerPays using swapAssetOut takerPays = swapAssetOut(pool, takerGets, tfee) @@ -495,7 +495,7 @@ def getAMMOfferStartWithTakerGets(pool, targetQuality, tfee): if Quality(amounts) < targetQuality: # Reduce takerGets by 0.9999x and recalculate reducedTakerGets = takerGets * 0.9999 - reducedTakerGets = toAmount(getIssue(pool.out), reducedTakerGets, roundingMode=downward) + reducedTakerGets = toAmount(getAsset(pool.out), reducedTakerGets, roundingMode=downward) reducedTakerPays = swapAssetOut(pool, reducedTakerGets, tfee) amounts = TAmounts(in=reducedTakerPays, out=reducedTakerGets) @@ -572,7 +572,7 @@ def getAMMOfferStartWithTakerPays(pool, targetQuality, tfee): # Round takerPays downward (minimizes offer, maximizes quality) # This has most impact when takerPays is XRP - takerPays = toAmount(getIssue(pool.in), takerPays, roundingMode=downward) + takerPays = toAmount(getAsset(pool.in), takerPays, roundingMode=downward) # Calculate takerGets using swapAssetIn takerGets = swapAssetIn(pool, takerPays, tfee) @@ -583,7 +583,7 @@ def getAMMOfferStartWithTakerPays(pool, targetQuality, tfee): if Quality(amounts) < targetQuality: # Reduce takerPays by 0.9999x and recalculate reducedTakerPays = takerPays * 0.9999 - reducedTakerPays = toAmount(getIssue(pool.in), reducedTakerPays, roundingMode=downward) + reducedTakerPays = toAmount(getAsset(pool.in), reducedTakerPays, roundingMode=downward) reducedTakerGets = swapAssetIn(pool, reducedTakerPays, tfee) amounts = TAmounts(in=reducedTakerPays, out=reducedTakerGets) @@ -596,7 +596,7 @@ def getAMMOfferStartWithTakerPays(pool, targetQuality, tfee): Calculate LP tokens to receive for a single-asset deposit.[^lp-tokens-out] -[^lp-tokens-out]: `lpTokensOut`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L26-L47) +[^lp-tokens-out]: `lpTokensOut`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L63-L82) ``` t1 = [R - (sqrt(f2^2 + R/f1) - f2)] / [1 + (sqrt(f2^2 + R/f1) - f2)] @@ -651,7 +651,7 @@ def lpTokensOut( Calculate required asset deposit for desired LP tokens.[^amm-asset-in] -[^amm-asset-in]: `ammAssetIn`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L60-L86) +[^amm-asset-in]: `ammAssetIn`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L95-L118) Equation 4 is the inverse of Equation 3 (lpTokensOut). We solve Equation 3 for the asset deposit ratio `R = assetDeposit / assetBalance`. @@ -764,7 +764,7 @@ def ammAssetIn( Calculate LP tokens to redeem for a single-asset withdrawal.[^lp-tokens-in] -[^lp-tokens-in]: `lpTokensIn`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L92-L113) +[^lp-tokens-in]: `lpTokensIn`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L124-L143) ``` t = T * (c - sqrt(c^2 - 4R)) / 2 @@ -807,7 +807,7 @@ def lpTokensIn( Calculate asset withdrawal for redeeming LP tokens.[^amm-asset-out] -[^amm-asset-out]: `ammAssetOut`: [`AMMHelpers.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/misc/detail/AMMHelpers.cpp#L125-L145) +[^amm-asset-out]: `ammAssetOut`: [`AMMHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/AMMHelpers.cpp#L155-L173) Equation 8 is the inverse of Equation 7 (lpTokensIn). We solve Equation 7 for the asset withdrawal amount. diff --git a/docs/amms/withdraw.md b/docs/amms/withdraw.md index e97f3d1..5da708d 100644 --- a/docs/amms/withdraw.md +++ b/docs/amms/withdraw.md @@ -34,7 +34,7 @@ AMM helpers use **token** as a subject in many function names. This refers to an 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. -[^applyGuts]: AMMWithdraw::applyGuts: [AMMWithdraw.cpp](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L298-L421) +[^applyGuts]: AMMWithdraw::applyGuts: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L307-L422) ## 2.1. applyGuts Pseudo-Code @@ -50,7 +50,7 @@ def applyGuts(sb: &Sandbox, tx: Transaction): ammAccountID = ammSle[sfAccount] # Get withdrawer's LP token balance - lpTokens = ammLPHolds(view, ammSle, account) + lpTokens = ammLPHolds(view, ammSle, accountID_) # Determine LP tokens to withdraw # For tfWithdrawAll and tfOneAssetWithdrawAll: use all LP tokens @@ -60,15 +60,16 @@ def applyGuts(sb: &Sandbox, tx: Transaction): # Adjust LP token balance for precision (with fixAMMv1_1) # This handles rounding issues for the last LP if rules.enabled(fixAMMv1_1): - verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account) + if not verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, accountID_): + return (tecAMM_INVALID_TOKENS, false) # Get current trading fee (with potential discount) - tfee = getTradingFee(view, ammSle, account) + tfee = getTradingFee(view, ammSle, accountID_) # Get current pool balances - # ZERO_IF_FROZEN: treat frozen assets as having zero balance - # ZERO_IF_UNAUTHORIZED: treat unauthorized MPT holders as having zero balance - currentBalances = ammHolds(sb, ammSle, amount, amount2, ZERO_IF_FROZEN, ZERO_IF_UNAUTHORIZED) + # 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) @@ -186,7 +187,7 @@ The modes are: Proportional withdrawal of pool assets for the amount of LP tokens.[^equalWithdrawTokens] -[^equalWithdrawTokens]: AMMWithdraw::equalWithdrawTokens: [AMMWithdraw.cpp](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L804-L887) +[^equalWithdrawTokens]: AMMWithdraw::equalWithdrawTokens: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -293,7 +294,7 @@ def equalWithdrawTokens( 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L915-L980) +[^equalWithdrawLimit]: AMMWithdraw::equalWithdrawLimit: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -413,7 +414,7 @@ Single-asset withdrawal modes allow users to withdraw only one asset instead of 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L988-L1024) +[^singleWithdraw]: AMMWithdraw::singleWithdraw: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L969-L1007) ### 5.1.1. singleWithdraw Pseudo-Code @@ -467,7 +468,7 @@ def singleWithdraw( Withdraw a single asset by redeeming specified LP tokens.[^singleWithdrawTokens] -[^singleWithdrawTokens]: AMMWithdraw::singleWithdrawTokens: [AMMWithdraw.cpp](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L1037-L1069) +[^singleWithdrawTokens]: AMMWithdraw::singleWithdrawTokens: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -517,11 +518,13 @@ def singleWithdrawTokens( > "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. +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` (minimum effective price) and optionally `Amount` (minimum withdrawal amount). Unlike deposits where users set a maximum effective price (to avoid overpaying), withdrawals use a minimum effective price constraint - 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`. +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`. + +[^singleWithdrawEPrice]: AMMWithdraw::singleWithdrawEPrice: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L1073-L1130) ### 5.3.1. singleWithdrawEPrice Pseudo-Code @@ -589,7 +592,7 @@ def singleWithdrawEPrice( 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. -[^withdraw]: AMMWithdraw::withdraw: [AMMWithdraw.cpp](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/AMMWithdraw.cpp#L471-L722) +[^withdraw]: AMMWithdraw::withdraw: [AMMWithdraw.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMWithdraw.cpp#L472-L709) 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. @@ -667,6 +670,19 @@ def withdraw( 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): @@ -701,10 +717,6 @@ def withdraw( if balanceToCheck < reserve: return (tecINSUFFICIENT_RESERVE, None) - # For MPTs, increment owner count immediately - if not isIOU(asset): - adjustOwnerCount(view, sleAccount, 1, journal) - return (tesSUCCESS, mptokenKey) # Helper function to create MPToken if needed diff --git a/docs/credentials/README.md b/docs/credentials/README.md index 8d9effe..f5bd213 100644 --- a/docs/credentials/README.md +++ b/docs/credentials/README.md @@ -30,17 +30,21 @@ 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 requiring an "accredited_investor" credential from a regulatory authority. When Alice wants to trade on this venue: -1. Alice obtains the credential: RegulatorAccountID sends a CredentialCreate transaction with Subject=AliceAccountID and CredentialType="accredited_investor" +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 - all without any transaction from RegulatorAccountID +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 (enabling payments from credential holders without individual preauthorization), self-issued authorization markers (issuer == subject), and tiered access control (different credential types representing different authorization levels from the same issuer). +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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/detail/ledger_entries.macro#L443). SubjectNode only set in the issuer != subject branch: [`Credentials.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Credentials.cpp#L156-L168) +[^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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/ledger_entries.macro#L445). SubjectNode only set in the issuer != subject branch: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L163-L174) ## 1.1. Terminology and Concepts @@ -50,7 +54,7 @@ Credentials can also be used for DepositAuth (enabling payments from credential **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**: Credentials must be explicitly accepted by the subject (via `CredentialAccept` transaction) before they can be used for authorization. This two-phase process (create then accept) ensures subjects control what credentials appear in their account and prevents unwanted credential issuance. +**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. @@ -127,9 +131,9 @@ This ensures each credential is uniquely identified by its (subject, issuer, typ | `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) | -| `SubjectNode` | UInt64 | Optional | Index of the subject's owner directory page (only present when issuer != subject)[^2] | | `IssuerNode` | UInt64 | Yes | Index of the issuer's owner directory page | -| `Flags` | UInt32 | Optional | Credential flags (see below) | +| `SubjectNode` | UInt64 | Optional | Index of the subject's owner directory page (only present when issuer != subject)[^2] | +| `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 | @@ -145,8 +149,8 @@ The `Flags` field can contain the following values: - 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: [`Credentials.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Credentials.cpp#L137-L169) -[^4]: Deletion authorization: [`Credentials.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Credentials.cpp#L244-L249) +[^3]: Credential added to both directories during creation: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/credentials/CredentialCreate.cpp#L147-L174) +[^4]: Deletion authorization: [`Credentials.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/credentials/CredentialDelete.cpp#L89-L94) - Self-issued credentials (issuer == subject) automatically have `lsfAccepted` set during creation. ### 2.1.3. Pseudo-accounts @@ -176,10 +180,10 @@ 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 (credential moved to subject'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` (currently 2 XRP on Mainnet). When a credential is deleted, the reserve is freed and the owner count decreases. +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. # 3. Transactions @@ -383,8 +387,12 @@ The sender includes the hashes of credentials they hold. During transaction proc 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 if any credential matches the destination's accepted credential specifications -5. Verifies the credential is not expired and has `lsfAccepted` flag set +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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L62-L65) **Example Flow**: diff --git a/docs/flow/README.md b/docs/flow/README.md index e945d8d..a9506cc 100644 --- a/docs/flow/README.md +++ b/docs/flow/README.md @@ -56,7 +56,7 @@ Each **step** is a unit of routing logic. They execute the operations needed to - **[DirectStepI](steps.md#2-directstepi)** - Transfers tokens between accounts via trust lines - **[XRPEndpointStep](steps.md#3-xrpendpointstep)** - Transfers XRP to/from source or destination - **[MPTEndpointStep](steps.md#4-mptendpointstep)** - Transfers MPT to/from source or destination -- **[BookStep](steps.md#5-bookstep)** - Converts currencies through order books and AMM pools +- **[BookStep](steps.md#5-bookstep)** - Converts currencies through order books and AMM pools. (The example below uses `BookStepII`. `BookStep` actually comes in multiple variants, named by their input/output asset pair. Examples include `BookStepII`, `BookStepXI`, and `BookStepMX`, one per XRP/IOU/MPT combination.) Once constructed, strands have no need for path elements anymore and they represent the complete list of steps required for a payment. However, one way to understand steps is as if they were connections between path elements. For example, this is how path elements (circles) will be spanned by strand steps (diamonds): @@ -93,7 +93,7 @@ flowchart LR Flow executes payments by converting paths into executable strands and consuming liquidity from the best-quality strands until the payment is complete. We'll illustrate the algorithm using an example: Alice wants to send up to 300 USD and Bob should receive 250 EUR. -This example uses "quality", which is effectivelly the exchange rate including transfer rates and fees. Note that quality is stored as in/out, so lower values are better, but the codebase inverts its comparison operators to make "higher quality" mean "better deal". See [Quality Representation](#21-quality) for details on how this works and the potential confusion it introduces. Composite quality is the product of qualities for all steps along the strand. +This example uses "quality", which is effectivelly the exchange rate including transfer rates and fees. Note that quality is stored as in/out, so lower values are better, but the codebase inverts its comparison operators to make "higher quality" mean "better deal". See [Quality Representation](#21-quality) for details on how this works and the potential confusion it introduces. Composite quality is the product of qualities for all steps along the strand[^composed-quality]. **Setup:** - Alice has 1000 USD with USD Issuer @@ -244,7 +244,7 @@ flowchart LR - [Payment transaction](../payments/README.md) - [OfferCreate transaction](../offers/README.md) crossing - RPC [path finding](../path_finding/README.md) endpoints - - CashCheck transaction + - CheckCash transaction - XChainBridge - **[toStrands](#4-tostrands)** is a function that converts a set of paths into strands, by calling `toStrand` on each one. - **[toStrand](#5-tostrand)** converts a single path to a strand through [path normalization](#51-path-normalization), [path to strand conversion](#52-path-to-strand-conversion), and [step generation](#53-step-generation) @@ -252,19 +252,19 @@ flowchart LR - **[Strand Flow](#7-single-strand-evaluation-strandflow)** (another function called `flow`, accepting a single `strand` as parameter) is implemented in `StrandFlow.h` in the paths/detail module[^strandflow-entrypoint]. In this document we refer to it as `strandFlow`. It evaluates a strand using the [two-pass method](#73-reverse-and-forward-passes). - **Finish Flow** (`finishFlow` function) cleans up after the execution is complete. -[^flow-entrypoint]: Flow entry point implementation: [`Flow.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Flow.cpp#L36) -[^strandsflow-entrypoint]: Iterative Strands Evaluation implementation: [`StrandFlow.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L552) -[^strandflow-entrypoint]: Strand Flow implementation: [`StrandFlow.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L86) -[^tostrands]: toStrands implementation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L601) -[^quality-rate]: Quality stored as normalize(input / output) via `getRate`: [`STAmount.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/STAmount.cpp#L451-L472), [`Quality.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Quality.cpp#L16-L18) -[^quality-comparison]: Inverted comparison operators (lower stored value = higher quality): [`Quality.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/Quality.h#L230-L244) -[^quality-increment]: Increment decreases stored value (higher quality), decrement increases it (lower quality): [`Quality.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Quality.cpp#L21-L53) -[^quality-no-improvement]: Quality anti-improvement check in `qualitiesSrcRedeems`: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L750-L762) -[^composed-quality]: `composed_quality` multiplies step rates: [`Quality.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Quality.cpp#L139-L161) +[^flow-entrypoint]: Flow entry point implementation: [`Flow.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/Flow.cpp#L45) +[^strandsflow-entrypoint]: Iterative Strands Evaluation implementation: [`StrandFlow.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L556) +[^strandflow-entrypoint]: Strand Flow implementation: [`StrandFlow.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L82) +[^tostrands]: toStrands implementation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L574) +[^quality-rate]: Quality stored as normalize(input / output) via `getRate`: [`STAmount.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/STAmount.cpp#L460-L481), [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Quality.cpp#L17-L18) +[^quality-comparison]: Inverted comparison operators (lower stored value = higher quality): [`Quality.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/Quality.h#L216-L230) +[^quality-increment]: Increment decreases stored value (higher quality), decrement increases it (lower quality): [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Quality.cpp#L21-L53) +[^quality-no-improvement]: Quality anti-improvement check in `qualitiesSrcRedeems`: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L738-L749) +[^composed-quality]: `composedQuality` multiplies step rates: [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Quality.cpp#L114-L131) **Step Implementations:** -Each step type inherits from the `Step` interface through a `StepImp` template base class and implements methods for reverse pass calculation (`revImp`), forward pass calculation (`fwdImp`), quality estimation (`qualityUpperBound`), and validation (`check`) - see [step methods documentation](steps.md#13-methods) for details. Each step type is implemented as a base class with derived classes for payments and offer crossing. Payment variants enforce trust line limits and the offer owner pays transfer fees. Offer crossing variants waive trust line limits for the destination step and the taker pays transfer fees. +Each step type inherits from the `Step` interface through a `StepImp` template base class and implements methods for reverse pass calculation (`revImp`), forward pass calculation (`fwdImp`), quality estimation (`qualityUpperBound`), and validation (`check`) - see [step methods documentation](steps.md#13-methods) for details. The `revImp`/`fwdImp`/`qualityUpperBound` methods back the `Step` virtual interface. `check` does not. It is a separate method defined on each concrete step type, including its payment and offer-crossing variants. Each step type is implemented as a base class with derived classes for payments and offer crossing. Payment variants enforce trust line limits and the offer owner pays transfer fees. Offer crossing variants waive trust line limits for the destination step and the taker pays transfer fees. # 2. Terminology and Concepts @@ -327,33 +327,33 @@ Each step type calculates quality differently: See the [steps documentation](steps.md) for detailed quality calculations. -[^xrp-quality]: XRPEndpointStep always returns `Quality{STAmount::uRateOne}`: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L239-L247) -[^xrp-quality-oc]: `qualityUpperBound` is in the base template `XRPEndpointStep` with no override in the offer crossing variant `XRPEndpointOfferCrossingStep`: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L172-L225) -[^mpt-quality-issues]: MPTEndpointStep applies transfer rate in `qualitiesSrcIssues` only when `redeems(prevStepDebtDirection)`: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L796-L816) -[^mpt-oc-prev-issues]: MPTEndpointOfferCrossingStep asserts previous step always issues: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L288-L299) -[^direct-quality-payment]: DirectIPaymentStep reads QualityIn/QualityOut from trust line fields: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L311-L348) -[^direct-quality-issues]: DirectStepI applies transfer rate in `qualitiesSrcIssues` only when `redeems(prevStepDebtDirection)`: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L765-L788) -[^direct-quality-oc]: DirectIOfferCrossingStep ignores trust line quality fields, always returns `QUALITY_ONE`: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L350-L356) -[^direct-oc-prev-issues]: DirectIOfferCrossingStep asserts previous step always issues: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L265-L276) -[^book-quality-payment]: BookPaymentStep `adjustQualityWithFees` applies input transfer fee when `redeems(prevStepDir)`: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L306-L332) -[^book-owner-pays]: Output transfer fee requires `ownerPaysTransferFee_`, which is only true for offer crossing: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L326-L328) -[^book-quality-oc]: BookOfferCrossingStep returns unmodified offer quality for CLOB and multi-path AMM: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L492-L526) -[^book-quality-oc-amm]: Single-path AMM input transfer fee during offer crossing requires `fixAMMv1_1`: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L511-L525) +[^xrp-quality]: XRPEndpointStep always returns `Quality{STAmount::kURateOne}`: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L253-L257) +[^xrp-quality-oc]: `qualityUpperBound` is in the base template `XRPEndpointStep` with no override in the offer crossing variant `XRPEndpointOfferCrossingStep`: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L190-L241) +[^mpt-quality-issues]: MPTEndpointStep applies transfer rate in `qualitiesSrcIssues` only when `redeems(prevStepDebtDirection)`: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L750-L768) +[^mpt-oc-prev-issues]: MPTEndpointOfferCrossingStep asserts previous step always issues: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L299-L310) +[^direct-quality-payment]: DirectIPaymentStep reads QualityIn/QualityOut from trust line fields: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L341-L380) +[^direct-quality-issues]: DirectStepI applies transfer rate in `qualitiesSrcIssues` only when `redeems(prevStepDebtDirection)`: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L752-L771) +[^direct-quality-oc]: DirectIOfferCrossingStep ignores trust line quality fields, always returns `QUALITY_ONE`: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L382-L388) +[^direct-oc-prev-issues]: DirectIOfferCrossingStep asserts previous step always issues: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L296-L307) +[^book-quality-payment]: BookPaymentStep `adjustQualityWithFees` applies input transfer fee when `redeems(prevStepDir)`: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L338-L362) +[^book-owner-pays]: Output transfer fee requires `ownerPaysTransferFee_`, which is only true for offer crossing: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L356-L358) +[^book-quality-oc]: BookOfferCrossingStep returns unmodified offer quality for CLOB and multi-path AMM: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L510-L546) +[^book-quality-oc-amm]: Single-path AMM input transfer fee during offer crossing requires `fixAMMv1_1`: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L529-L545) The engine uses quality in two phases. Before executing a strand, it computes an estimated quality upper bound (the product of step quality estimates) to sort strands from best to worst and to filter out strands that fall below the `limitQuality` threshold. After executing a strand, it computes the actual quality from the execution results (actual input / actual output) and checks it against `limitQuality` again. The estimate and actual quality can differ because offers may be unfunded, balances may have changed, or rounding may have occurred. See [qualityUpperBound](#62-qualityupperbound) for details on strand sorting and filtering. ## 2.2. Debt Direction (Redeeming vs Issuing) -Every step must know whether its source account is **redeeming** or **issuing** relative to its destination. +Every step must know whether its source account is **redeeming** or **issuing**[^debt-direction-enum] relative to its destination. Debt direction controls which quality adjustments and fees apply to a step. The same step between the same two accounts can produce different exchange rates depending on which direction the debt is moving. Transfer fees, QualityIn/QualityOut, and the anti-improvement constraint each apply only in one debt direction, not the other. A step also needs to know the **previous step's** debt direction. Certain fees are charged when the current step is issuing but the previous step was redeeming. Each step therefore propagates its debt direction to the next step via `qualityUpperBound` and the `revImp`/`fwdImp` methods. -See [step quality implementation](steps.md) for how each factor is conditionally applied based on debt direction and how debt direction is determined. +See [step quality implementation](steps.md) for how each factor is conditionally applied based on debt direction and how debt direction is determined[^debt-direction-direct]. -[^debt-direction-enum]: `DebtDirection` enum definition: [`Steps.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/Steps.h#L25) -[^debt-direction-direct]: DirectStepI determines debt direction via `accountHolds`: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L479-L482) +[^debt-direction-enum]: `DebtDirection` enum definition: [`Steps.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/Steps.h#L21) +[^debt-direction-direct]: DirectStepI determines debt direction via `accountHolds`: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L497-L498) # 3. Flow @@ -432,7 +432,7 @@ def flow( # Evaluate strands to consume liquidity flowResult = strandsFlow( - baseView, + sb, strands, deliver, partialPayment, @@ -443,7 +443,7 @@ def flow( ) # Package results and clean up state - finishFlow(baseView, flowResult) + finishFlow(sb, flowResult) return flowResult.ter, flowResult.actualIn, flowResult.actualOut, flowResult.offersToRemove ``` @@ -458,7 +458,7 @@ A single instance is created at the start of the `flow()` function and passed to | Field | Type | Description | |--------------|-----------|------------------------------------------------------------------------------| -| `account_` | AccountID | Transaction sender account | +| `accountID_` | AccountID | Transaction sender account | | `multiPath_` | bool | Whether payment has multiple strands (set to `true` if `strands.size() > 1`) | | `ammUsed_` | bool | Whether AMM offer was consumed in current iteration | | `ammIters_` | uint16 | Counter of iterations where AMM was consumed (max 30) | @@ -471,9 +471,11 @@ The `multiPath_` flag is set based on the number of strands after `toStrands()` ammContext.setMultiPath(strands.size() > 1); ``` -This flag determines which of the two AMM offer sizing strategies is used. See [BookStep: Offer Generation Strategies](steps.md#543-offer-generation-strategies) for details. +This flag determines which of the two AMM offer sizing strategies is used. See [BookStep: Offer Generation Strategies](steps.md#543-offer-generation-strategies) for details. This is only the initial value: `strandsFlow` re-computes `multiPath_` on each iteration from the count of currently-active (non-dry) strands[^multipath-reset], so the flag reflects live liquidity rather than just the post-`toStrands()` total. + +[^multipath-reset]: `strandsFlow` re-sets multiPath each iteration from the active-strand count: [`StrandFlow.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L640) -AMM offers can be consumed in at most 30 iterations (`MaxIterations = 30`). The `ammIters_` counter increments each time an AMM offer is consumed. Once `ammIters_ >= 30`, `maxItersReached()` returns true and no more AMM offers are generated. +AMM offers can be consumed in at most 30 iterations (`kMaxIterations = 30`). The `ammIters_` counter increments each time an AMM offer is consumed. Once `ammIters_ >= 30`, `maxItersReached()` returns true and no more AMM offers are generated. ## 3.3. Domain Payments @@ -483,8 +485,8 @@ When a payment or offer crossing includes a `domainID` parameter, the Flow engin **Access Verification**: Before the Flow engine executes, domain access is verified during transaction preclaim. For Payment transactions, both the sender (Account) and receiver (Destination) must be "in domain"[^domain-access-payment] - either the domain owner or hold a valid accepted credential that matches one of the domain's AcceptedCredentials entries. For OfferCreate transactions, the offer creator needs to be in domain.[^domain-access-offer] If verification fails, the transaction fails with `tecNO_PERMISSION` before Flow begins execution. -[^domain-access-payment]: Payment domain access verification: [`Payment.cpp:373-382`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L373-L382) -[^domain-access-offer]: OfferCreate domain access verification: [`CreateOffer.cpp:215-220`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L215-L220) +[^domain-access-payment]: Payment domain access verification: [`Payment.cpp:392-399`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L392-L399) +[^domain-access-offer]: OfferCreate domain access verification: [`OfferCreate.cpp:241-245`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L241-L245) Domain membership is verified using the `accountInDomain()` function, which: 1. Checks if the account is the domain owner (immediate access) @@ -502,7 +504,7 @@ Once domain access is verified and Flow begins execution, it has two key implica **Order Book Isolation**: BookSteps are constructed with the domain ID, which affects order book directory lookup. The book directory hash includes the domain ID: `hash(BOOK_NAMESPACE, asset_in, asset_out, domainID)`. This ensures that only offers within the specified domain can be discovered and consumed. -Domain payments and offer crossing cannot consume AMM liquidity. When `domainID` is specified, BookSteps do not generate AMM offers. +Domain payments and offer crossing cannot consume AMM liquidity. The BookStep still builds its AMM liquidity object regardless of domain; what is suppressed is AMM *consumption*, short-circuited in `tryAMM`, which returns early when the book is domain-scoped (`if (book_.domain)`). An AMM offer can still contribute to a strand's quality *estimate* (`qualityUpperBound`/`tip` do not check the domain); only consumption is blocked. For example: @@ -510,7 +512,7 @@ For example: - Flow engine creates BookSteps with domainID - BookSteps look up domain-specific order book directories - Only domain offers and hybrid offers (in domain book) can be consumed -- AMM is NOT accessible +- AMM liquidity is not consumed (suppressed in `tryAMM`; it may still factor into quality estimation) 2. Open Payment or Offer Crossing (domainID not set): - Flow engine creates BookSteps without domainID @@ -549,7 +551,7 @@ sequenceDiagram toStrands->>Strands: Add user-provided paths end Strands-->>toStrands: Collection with strands - alt strands are empty + alt no default path AND no user paths toStrands-->Caller: Return temRIPPLE_EMPTY end loop For each path in paths @@ -576,7 +578,7 @@ sequenceDiagram `toStrand`[^tostrand] first normalizes the path and then converts a single path into a strand (sequence of steps). -[^tostrand]: toStrand implementation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L178) +[^tostrand]: toStrand implementation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L170) **Paths and Path Elements:** @@ -604,9 +606,9 @@ Before normalization begins, the payment parameters are validated[^payment-valid After validation, `toStrand` normalizes the path[^path-normalization] before converting path elements to strand steps. -[^payment-validation]: Payment parameter validation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L192-L204) -[^path-validation]: Path element validation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L206-L250) -[^path-normalization]: Path normalization implementation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L269-L338) +[^payment-validation]: Payment parameter validation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L184-L190) +[^path-validation]: Path element validation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L197-L238) +[^path-normalization]: Path normalization implementation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L257-L321) The path is composed of path elements, and normalizing a path means that path elements that are not explicitly defined but are necessary are added to the path. @@ -643,12 +645,12 @@ The normalization process constructs a complete path by adding implied elements - Account: `dst` - Condition: The last element in normPath is NOT an account with `dst` -[^source-element]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L274-L286 -[^sendmax-issuer]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L288-L298 -[^user-path-elements]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L300-L301 -[^delivery-conversion]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L303-L316 -[^delivery-issuer]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L323-L329 -[^destination-account]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L331-L337 +[^source-element]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L262-L273 +[^sendmax-issuer]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L275-L284 +[^user-path-elements]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L286-L287 +[^delivery-conversion]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L289-L301 +[^delivery-issuer]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L308-L313 +[^destination-account]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L315-L320 Take for an example a payment in which Alice wants to deliver 100 MPT to Bob and spend USD (IOU) issued by Issuer for it. @@ -676,7 +678,7 @@ Here are some example normalization scenarios: The conversion process iterates through adjacent pairs of path elements in the normalized path, generating executable steps[^path-to-strand]. Path elements do not map one-to-one to steps; a single pair may generate multiple steps when intermediate issuer hops are required. -[^path-to-strand]: Path to strand conversion implementation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L340-L597) +[^path-to-strand]: Path to strand conversion implementation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L323-L570) **Core Algorithm:** @@ -685,7 +687,7 @@ The conversion process iterates through adjacent pairs of path elements in the n - For IOUs: Set to `Asset(currency, src)` - For MPTs: Set to the MPTIssue -[^curasset-init]: curAsset initialization: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L252-L262) +[^curasset-init]: curAsset initialization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L240-L250) 2. **Iterate through path element pairs** (current and next)[^path-element-iteration] and for each adjacent pair of path elements, the algorithm: - Tracks the current asset being transferred by updating `curAsset` based on the current element's fields @@ -693,7 +695,7 @@ The conversion process iterates through adjacent pairs of path elements in the n - Calls [`toStep()`](#53-step-generation) to generate the primary step that transfers value between the pair - Accumulates all generated steps into the final strand -[^path-element-iteration]: Path element pair iteration: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L380-L530) +[^path-element-iteration]: Path element pair iteration: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L363-L506) **Implied Step Injection:** @@ -702,17 +704,17 @@ In certain cases, additional steps must be inserted instead of calling `toStep() - **Offer -> Account (XRP output)**[^xrp-endpoint-inject]: When an offer outputs XRP and the next element is an account: if this is the last pair, insert `XRPEndpointStep(next)`; otherwise return `temBAD_PATH` since XRP can only appear at strand endpoints - **Offer -> Account (IOU output, next is not issuer)**[^direct-step-inject]: When an offer outputs an IOU and the next account is not the issuer, insert `DirectStepI(issuer -> next)` and skip `toStep()` call -[^xrp-endpoint-inject]: XRP endpoint injection: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L484-L496) -[^direct-step-inject]: Direct step injection after offer: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L498-L506) +[^xrp-endpoint-inject]: XRP endpoint injection: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L464-L471) +[^direct-step-inject]: Direct step injection after offer: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L475-L482) ### 5.2.1. Strand Validation Context The validation context (`StrandContext`)[^strandcontext] serves two purposes during strand construction: -[^strandcontext]: StrandContext struct definition: [`Steps.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/Steps.h#L521) -[^seenassets]: seenDirectAssets and seenBookOuts initialization: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L355-L378) +[^strandcontext]: StrandContext struct definition: [`Steps.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/Steps.h#L517) +[^seenassets]: seenDirectAssets and seenBookOuts initialization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L338-L343) -1. **Provides execution parameters**: Carries the ledger view, source/destination accounts, quality limits, flags (ownerPaysTransferFee, offerCrossing, etc.), AMM context, and strandDeliver domain needed for step construction and validation +1. **Provides execution parameters**: Carries the ledger view, source/destination accounts, quality limits, flags (ownerPaysTransferFee, offerCrossing, etc.), AMM context, the delivered asset (`strandDeliver`), and the order-book domain (`domainID`) needed for step construction and validation 2. **Detects invalid loops**: Tracks which assets have been used[^seenassets] to prevent the same account from appearing multiple times in the same asset and role within a single strand @@ -730,9 +732,9 @@ This two-index separation allows the same asset to appear legitimately in both r Alice (holder) -> Issuer (destination of first step) -> Issuer (source of second step) -> Bob (holder) ``` -The issuer appears twice but in different roles: -1. First MPTEndpointStep: Issuer is the **destination** (Alice redeems MPT to issuer, decreasing OutstandingAmount) - recorded in `seenDirectAssets[1]` -2. Last MPTEndpointStep: Issuer is the **source** (issuer issues MPT to Bob, increasing OutstandingAmount) - recorded in `seenDirectAssets[0]` +The issuer appears twice but in different roles. Note that `MPTEndpointStep` keys loop detection by **position** (`isFirst` records into `seenDirectAssets[0]`, `isLast` into `seenDirectAssets[1]`) using the bare MPTIssue, rather than by source/destination role the way `DirectStepI` does: +1. First MPTEndpointStep (`isFirst`): Issuer is the **destination** (Alice redeems MPT to issuer, decreasing OutstandingAmount) - recorded in `seenDirectAssets[0]` +2. Last MPTEndpointStep (`isLast`): Issuer is the **source** (issuer issues MPT to Bob, increasing OutstandingAmount) - recorded in `seenDirectAssets[1]` This is valid because the issuer acts as an intermediary, receiving tokens in one step and sending them in another. The two indices prevent invalid loops (issuer as source twice, or destination twice) while allowing valid intermediary routing. @@ -744,7 +746,7 @@ When a step's `check()` method validates, it verifies its asset hasn't been seen After path normalization and the main conversion loop (which handles implied steps), the `toStep` function[^tostep] is called for each adjacent pair of path elements to create the appropriate step type. This function acts as a factory that examines the characteristics of the two elements in the pair and selects the correct step implementation. -[^tostep]: toStep implementation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L58) +[^tostep]: toStep implementation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L66) The choice of step type depends on three key factors: - **Position in strand**: Whether the strand has no steps yet (`ctx.isFirst`, set via `strand.empty()`[^isfirst]) or this is the last element pair in the normalized path (`ctx.isLast`), which affects how XRP is handled @@ -777,23 +779,23 @@ When the second element in the pair is an order book, `toStep` creates `BookStep - IOU -> MPT: `BookStepIM`[^bookstep-im] - XRP -> XRP: Error (`temBAD_PATH`)[^bookstep-xrp-xrp] -[^isfirst]: isFirst initialization: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L739) -[^first-xrp]: First XRP element check: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L66-L71) -[^last-xrp]: Last XRP element check: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L73-L74) -[^account-account]: Account to account check: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L91-L108) -[^directstepi]: DirectStepI creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L101-L107) -[^mptendpointstep]: MPTEndpointStep creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L94-L100) -[^offer-account]: Offer to account unreachable: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L110-L119) -[^offer-element]: Offer element assertion: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L138) -[^bookstep-ix]: BookStepIX creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L144) -[^bookstep-xi]: BookStepXI creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L152) -[^bookstep-ii]: BookStepII creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L172) -[^bookstep-mx]: BookStepMX creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L143) -[^bookstep-xm]: BookStepXM creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L150) -[^bookstep-mm]: BookStepMM creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L163) -[^bookstep-mi]: BookStepMI creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L160) -[^bookstep-im]: BookStepIM creation: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L169) -[^bookstep-xrp-xrp]: XRP to XRP error: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L132-L136) +[^isfirst]: isFirst initialization: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L709) +[^first-xrp]: First XRP element check: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L74-L78) +[^last-xrp]: Last XRP element check: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L80-L81) +[^account-account]: Account to account check: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L98-L108) +[^directstepi]: DirectStepI creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L105-L107) +[^mptendpointstep]: MPTEndpointStep creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L101-L104) +[^offer-account]: Offer to account unreachable: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L110-L118) +[^offer-element]: Offer element assertion: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L136) +[^bookstep-ix]: BookStepIX creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L142) +[^bookstep-xi]: BookStepXI creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L149) +[^bookstep-ii]: BookStepII creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L164) +[^bookstep-mx]: BookStepMX creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L141) +[^bookstep-xm]: BookStepXM creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L148) +[^bookstep-mm]: BookStepMM creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L158) +[^bookstep-mi]: BookStepMI creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L156) +[^bookstep-im]: BookStepIM creation: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L162) +[^bookstep-xrp-xrp]: XRP to XRP error: [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L130-L134) **Example: IOU to different IOU Payment with MPT bridge** @@ -982,6 +984,8 @@ def toStrand(...): # If offer outputs to account that's not the issuer if curAsset.issuer != next.getAccountId() and not isXRP(next.getAccountID()): if isXRP(curAsset): + if i != len(normPath) - 2: + return temBAD_PATH # XRP can only appear at a strand endpoint # Last step: insert XRP endpoint step result.add(XRPEndpointStep(next.getAccountId())) else: @@ -1099,7 +1103,7 @@ Each step type has specific validation requirements detailed in the [Steps docum > Iterative Strands Evaluation is a function called `flow` in `rippled`[^strandsflow]. Here we call it `strandsFlow` to differentiate between the primary `flow` function and `strandFlow` function which executes a single strand. -[^strandsflow]: strandsFlow implementation: [`StrandFlow.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L552) +[^strandsflow]: strandsFlow implementation: [`StrandFlow.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L556) So far, the Flow engine has been constructing a possible set of steps that a payment can take. `Iterative Strands Evaluation` is interested in executing those strands to see if they have enough liquidity, and which ones produce the best quality. It executes a payment across multiple strands using iterative liquidity consumption, with all changes tracked in a PaymentSandbox (in-memory ledger view) that can be committed or discarded. @@ -1133,8 +1137,8 @@ Each iteration performs: - If the strand is dry (no liquidity available), reject it and continue to the next strand - Otherwise, execute the strand: - Consume liquidity from the strand - - Decrement `remainingOut` by the delivered amount - - Decrement `remainingIn` by the consumed amount (if `sendMax` is defined) + - Update `remainingOut` (recomputed as `outReq - sum(deliveredOut)`, summed smallest-to-largest for precision, not decremented in place) + - Update `remainingIn` likewise (`sendMax - sum(consumedIn)`, if `sendMax` is defined) - If the strand still has liquidity available after consumption, add it to the queue for the next iteration - Add all subsequent unevaluated strands to the queue for the next iteration - Continue to the next iteration (do not evaluate remaining strands in this iteration) @@ -1395,13 +1399,13 @@ def strandsFlow( ## 6.2. qualityUpperBound -The [`StrandFlow::qualityUpperBound`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L330-L344) function computes the composite quality of a strand. It is used in two places during `strandsFlow` execution: +The [`StrandFlow::qualityUpperBound`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L325-L343) function computes the composite quality of a strand. It is used in two places during `strandsFlow` execution: 1. **Sorting**: At the start of each `strandsFlow` iteration (the outer loop that continues until all liquidity is consumed or the payment is satisfied) to rank remaining strands by quality[^sorting-code] 2. **Filtering**: In the inner loop that evaluates strands in quality order within each iteration, to reject strands below the `limitQuality` threshold[^filtering-code] -[^sorting-code]: See [`qualityUpperBound` call in `ActiveStrands::activateNext` in StrandFlow.h](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L451) -[^filtering-code]: See [`qualityUpperBound` call in `flow` function in StrandFlow.h](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L681) +[^sorting-code]: See [`qualityUpperBound` call in `ActiveStrands::activateNext` in StrandFlow.h](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L463) +[^filtering-code]: See [`qualityUpperBound` call in `flow` function in StrandFlow.h](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L671) **Quality Calculation:** @@ -1427,12 +1431,12 @@ There is no guarantee that this quality will actually be achieved. For example, ## 6.3. limitOut -The [`limitOut`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L356-L402) function calculates the maximum output amount for a strand containing AMM liquidity that maintains a specified quality threshold. This is an AMM-specific optimization applied only when:[^limitout-call] +The [`limitOut`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L355-L414) function calculates the maximum output amount for a strand containing AMM liquidity that maintains a specified quality threshold. This is an AMM-specific optimization applied only when:[^limitout-call] - There is a single strand available - A `limitQuality` threshold is specified - The strand contains at least one AMM step (a `BookStep` with AMM liquidity) -[^limitout-call]: See [`limitOut` call in `flow` function in StrandFlow.h](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L651) +[^limitout-call]: See [`limitOut` call in `flow` function in StrandFlow.h](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L647) Unlike CLOB offers which have fixed quality, AMM pools exhibit dynamic quality that degrades as more liquidity is consumed. AMMs use a constant product invariant: `poolGets * poolPays = (poolGets + in * cfee) * (poolPays - out)`. Solving for `in` and substituting into `q = out / in` gives: @@ -1464,15 +1468,15 @@ The `outFromAvgQ` function inverts this relationship to solve for output: `out = A special case is handled when there is no positive `out` for the desired `limitQuality`, when `outFromAvgQ` returns a null result to indicate this is the case. -[amm-m]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/QualityFunction.h#L82 -[amm-b]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/QualityFunction.h#L83 -[cfee]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/AMMCore.h#L91-L94 -[clob-m]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/QualityFunction.cpp#L15 -[clob-b]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/QualityFunction.cpp#L19 -[outfromavgq]: https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/QualityFunction.cpp#L32-L43 -[^constant-quality]: See default `Step::getQualityFunc` implementation which returns `CLOBLikeTag` in [Steps.h](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/Steps.h#L296-L304) -[^dynamic-quality]: See `BookStep::getQualityFunc` implementation which checks `isConst()` and returns AMM quality function in [BookStep.cpp](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L578-L616), and `AMMOffer::getQualityFunc` in [AMMOffer.cpp](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMOffer.cpp#L114-L120) -[^isconst]: Quality functions constructed with `CLOBLikeTag` are constant (return `true` for `isConst()`), while those constructed with `AMMTag` are non-constant. See [QualityFunction.h](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/QualityFunction.h#L61-L64) +[amm-m]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/QualityFunction.h#L78 +[amm-b]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/QualityFunction.h#L79 +[cfee]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/AMMCore.h#L87-L90 +[clob-m]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/QualityFunction.cpp#L14 +[clob-b]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/QualityFunction.cpp#L18 +[outfromavgq]: https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/QualityFunction.cpp#L31-L42 +[^constant-quality]: See default `Step::getQualityFunc` implementation which returns `CLOBLikeTag` in [Steps.h](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/Steps.h#L289-L295) +[^dynamic-quality]: See `BookStep::getQualityFunc` implementation which checks `isConst()` and returns AMM quality function in [BookStep.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L590-L621), and `AMMOffer::getQualityFunc` in [AMMOffer.cpp](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMOffer.cpp#L124-L129), which returns the dynamic `AMMTag` quality function only in single-path mode (in multi-path mode it returns a constant `CLOBLikeTag` function) +[^isconst]: Quality functions constructed with `CLOBLikeTag` are constant (return `true` for `isConst()`), while those constructed with `AMMTag` are non-constant. See [QualityFunction.h](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/QualityFunction.h#L57-L60) # 7. Single Strand Evaluation (strandFlow) @@ -1480,7 +1484,7 @@ A special case is handled when there is no positive `out` for the desired `limit `Single Strand Evaluation` returns how much of the `out` output parameter a strand can produce without violating `maxIn` constraint. -[^strandflow-impl]: See `flow` function (single strand evaluation) in [StrandFlow.h:86-286](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L86-L286) +[^strandflow-impl]: See `flow` function (single strand evaluation) in [StrandFlow.h:82-281](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L82-L281) The inputs to this function are: @@ -1561,7 +1565,7 @@ It returns, in our pseudocode: Evaluating steps in isolation is insufficient. `strandFlow` must determine the effective inputs and outputs for the entire sequence of steps. Each step can compute its capacity, but only when given specific input or output amounts - a step cannot independently determine how much it should process without knowing the requirements of adjacent steps. To solve this, Flow uses a two-pass evaluation strategy. The reverse pass works backwards from the destination to calculate possible and required inputs and execute the payment. -The forward pass, if reverse pass showed that the maximum input was limited (due to liquidity, quality or SendMax constraints), then calculates the output for the possible inputs that the reverse pass found. +The forward pass runs when the reverse pass found a limiting step before the last step, whether the limit came from `maxIn`/SendMax on the first step or from liquidity/quality on any intermediate step. It then computes the actual output for the inputs the reverse pass settled on. **Reverse Pass:** @@ -1770,6 +1774,7 @@ def strandFlow( # Reexecute reverse direction with reduced stepOut stepOut = r.out r = strand[i].rev(sb, afView, offersToRemove, stepOut) + limitStepOut = r.out if r.out == 0: # Reducing desired **out** amount can end up with an **in** that is so tiny that it rounds the output to 0. @@ -1808,7 +1813,7 @@ The Flow engine performs validation during two main phases: path conversion and ## 8.1. Path Conversion Errors[^path-conversion-errors] -[^path-conversion-errors]: Path conversion errors (toStrands): [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L601-L714) +[^path-conversion-errors]: Path conversion errors (toStrands): [`PaySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L574-L684) These errors occur in `toStrands` when converting the provided paths into strands: @@ -1822,7 +1827,7 @@ These errors occur during payment execution from different sources: ### 8.2.1. StrandFlow Errors[^strandflow-errors] -[^strandflow-errors]: StrandFlow errors: [`StrandFlow.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StrandFlow.h#L640-L850) +[^strandflow-errors]: StrandFlow errors: [`StrandFlow.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StrandFlow.h#L635-L820) Errors returned by the main flow execution logic: @@ -1833,7 +1838,7 @@ Errors returned by the main flow execution logic: ### 8.2.2. Step Validation Errors[^step-errors] -[^step-errors]: Step validation errors: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp), [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp), [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp), [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp) +[^step-errors]: Step validation errors: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp), [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp), [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp), [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp) Errors returned by individual step implementations during strand construction or execution: @@ -1846,12 +1851,12 @@ Errors returned by individual step implementations during strand construction or - `tecLOCKED`: MPT validation failure (see section 8.3) - `tecNO_PERMISSION`: MPT validation failure (see section 8.3) - `tecOBJECT_NOT_FOUND`: MPT validation failure (see section 8.3) -- `tecINTERNAL`: +- `tecINTERNAL` (raised by `RippleCalc.cpp` / `AMMHelpers.cpp`, not the step files): - AMM freeze lookup fails since there is no AMM ledger item - Exception thrown during flow execution ## 8.3. MPT-Specific Validations[^mpt-validations] -[^mpt-validations]: MPT validations in Flow steps: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L746-L753), [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L368-L383) +[^mpt-validations]: MPT validations in Flow steps: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L744-L747), [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L374-L375) -When a cross-currency payment path includes MPT assets (with [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment enabled), the Flow engine validates MPTs using [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for details on validation logic and error conditions. +When a cross-currency payment path includes MPT assets (with [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment enabled), the Flow engine validates MPTs using [`canTrade`](../mpts/README.md#361-cantrade). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for details on validation logic and error conditions. diff --git a/docs/flow/steps.md b/docs/flow/steps.md index cb336b7..ac421b8 100644 --- a/docs/flow/steps.md +++ b/docs/flow/steps.md @@ -84,14 +84,14 @@ The **fundamental types** of steps in `rippled`: During [path to strand conversion](README.md#52-path-to-strand-conversion), Flow examines each pair of adjacent path elements to determine what type of step connects them. The [toStep function](README.md#53-step-generation) examines element characteristics (accounts vs order books, currency types, position in strand) and calls the appropriate factory function. -- `make_DirectStepI`[^make-directstepi] for account-to-account IOU transfers -- `make_BookStepII`[^make-bookstepii], `make_BookStepIX`[^make-bookstepix], `make_BookStepXI`[^make-bookstepxi], `make_BookStepMM`[^make-bookstepmm], `make_BookStepMX`[^make-bookstepmx], `make_BookStepXM`[^make-bookstepxm], `make_BookStepMI`[^make-bookstepmi], `make_BookStepIM`[^make-bookstepim] for order book conversions +- `makeDirectStepI`[^make-directstepi] for account-to-account IOU transfers +- `makeBookStepIi`[^make-bookstepii], `makeBookStepIx`[^make-bookstepix], `makeBookStepXi`[^make-bookstepxi], `makeBookStepMm`[^make-bookstepmm], `makeBookStepMx`[^make-bookstepmx], `makeBookStepXm`[^make-bookstepxm], `makeBookStepMi`[^make-bookstepmi], `makeBookStepIm`[^make-bookstepim] for order book conversions - `make_XRPEndpointStep`[^make-xrpendpointstep] for XRP source/destination steps - `make_MPTEndpointStep`[^make-mptendpointstep] for MPT source/destination steps Steps handle different asset type combinations - XRP, IOU and MPT. In `rippled`, these are represented by different types (`XRPAmount`, `IOUAmount`, `MPTAmount`) with different arithmetic and precision characteristics. -**BookStep** uses template parameters to handle multiple asset type combinations. The factory functions (`make_BookStepII`, `make_BookStepIX`, etc.) instantiate the same `BookStep` template with the appropriate type parameters based on the input and output asset types. +**BookStep** uses template parameters to handle multiple asset type combinations. The factory functions (`makeBookStepIi`, `makeBookStepIx`, etc.) instantiate the same `BookStep` template with the appropriate type parameters based on the input and output asset types. Flow executes in one of two contexts, determined by the transaction type that invokes it: @@ -105,51 +105,51 @@ The context affects step behavior throughout the flow process. Steps receive the | Alias | Concrete class (definition) | Input | Output | Factory | |------------------------------|--------------------------------|-------------|-------------|-------------------------| -| DirectStepI (payment) | `DirectIPaymentStep`[^directipaymentstep] | `IOUAmount`[^iouamount] | `IOUAmount` | `make_DirectStepI`[^make-directstepi] | -| DirectStepI (offer crossing) | `DirectIOfferCrossingStep`[^directioffercrossingstep] | `IOUAmount` | `IOUAmount` | `make_DirectStepI` | -| BookStepII (payment) | `BookPaymentStep`[^bookpaymentstep] | `IOUAmount` | `IOUAmount` | `make_BookStepII`[^make-bookstepii] | -| BookStepII (offer crossing) | `BookOfferCrossingStep`[^bookoffercrossingstep] | `IOUAmount` | `IOUAmount` | `make_BookStepII` | -| BookStepIX (payment) | `BookPaymentStep` | `IOUAmount` | `XRPAmount`[^xrpamount] | `make_BookStepIX`[^make-bookstepix] | -| BookStepIX (offer crossing) | `BookOfferCrossingStep` | `IOUAmount` | `XRPAmount` | `make_BookStepIX` | -| BookStepXI (payment) | `BookPaymentStep` | `XRPAmount` | `IOUAmount` | `make_BookStepXI`[^make-bookstepxi] | -| BookStepXI (offer crossing) | `BookOfferCrossingStep` | `XRPAmount` | `IOUAmount` | `make_BookStepXI` | -| BookStepMM (payment) | `BookPaymentStep` | `MPTAmount`[^mptamount] | `MPTAmount` | `make_BookStepMM`[^make-bookstepmm] | -| BookStepMM (offer crossing) | `BookOfferCrossingStep` | `MPTAmount` | `MPTAmount` | `make_BookStepMM` | -| BookStepMX (payment) | `BookPaymentStep` | `MPTAmount` | `XRPAmount` | `make_BookStepMX`[^make-bookstepmx] | -| BookStepMX (offer crossing) | `BookOfferCrossingStep` | `MPTAmount` | `XRPAmount` | `make_BookStepMX` | -| BookStepXM (payment) | `BookPaymentStep` | `XRPAmount` | `MPTAmount` | `make_BookStepXM`[^make-bookstepxm] | -| BookStepXM (offer crossing) | `BookOfferCrossingStep` | `XRPAmount` | `MPTAmount` | `make_BookStepXM` | -| BookStepMI (payment) | `BookPaymentStep` | `MPTAmount` | `IOUAmount` | `make_BookStepMI`[^make-bookstepmi] | -| BookStepMI (offer crossing) | `BookOfferCrossingStep` | `MPTAmount` | `IOUAmount` | `make_BookStepMI` | -| BookStepIM (payment) | `BookPaymentStep` | `IOUAmount` | `MPTAmount` | `make_BookStepIM`[^make-bookstepim] | -| BookStepIM (offer crossing) | `BookOfferCrossingStep` | `IOUAmount` | `MPTAmount` | `make_BookStepIM` | +| DirectStepI (payment) | `DirectIPaymentStep`[^directipaymentstep] | `IOUAmount`[^iouamount] | `IOUAmount` | `makeDirectStepI`[^make-directstepi] | +| DirectStepI (offer crossing) | `DirectIOfferCrossingStep`[^directioffercrossingstep] | `IOUAmount` | `IOUAmount` | `makeDirectStepI` | +| BookStepII (payment) | `BookPaymentStep`[^bookpaymentstep] | `IOUAmount` | `IOUAmount` | `makeBookStepIi`[^make-bookstepii] | +| BookStepII (offer crossing) | `BookOfferCrossingStep`[^bookoffercrossingstep] | `IOUAmount` | `IOUAmount` | `makeBookStepIi` | +| BookStepIX (payment) | `BookPaymentStep` | `IOUAmount` | `XRPAmount`[^xrpamount] | `makeBookStepIx`[^make-bookstepix] | +| BookStepIX (offer crossing) | `BookOfferCrossingStep` | `IOUAmount` | `XRPAmount` | `makeBookStepIx` | +| BookStepXI (payment) | `BookPaymentStep` | `XRPAmount` | `IOUAmount` | `makeBookStepXi`[^make-bookstepxi] | +| BookStepXI (offer crossing) | `BookOfferCrossingStep` | `XRPAmount` | `IOUAmount` | `makeBookStepXi` | +| BookStepMM (payment) | `BookPaymentStep` | `MPTAmount`[^mptamount] | `MPTAmount` | `makeBookStepMm`[^make-bookstepmm] | +| BookStepMM (offer crossing) | `BookOfferCrossingStep` | `MPTAmount` | `MPTAmount` | `makeBookStepMm` | +| BookStepMX (payment) | `BookPaymentStep` | `MPTAmount` | `XRPAmount` | `makeBookStepMx`[^make-bookstepmx] | +| BookStepMX (offer crossing) | `BookOfferCrossingStep` | `MPTAmount` | `XRPAmount` | `makeBookStepMx` | +| BookStepXM (payment) | `BookPaymentStep` | `XRPAmount` | `MPTAmount` | `makeBookStepXm`[^make-bookstepxm] | +| BookStepXM (offer crossing) | `BookOfferCrossingStep` | `XRPAmount` | `MPTAmount` | `makeBookStepXm` | +| BookStepMI (payment) | `BookPaymentStep` | `MPTAmount` | `IOUAmount` | `makeBookStepMi`[^make-bookstepmi] | +| BookStepMI (offer crossing) | `BookOfferCrossingStep` | `MPTAmount` | `IOUAmount` | `makeBookStepMi` | +| BookStepIM (payment) | `BookPaymentStep` | `IOUAmount` | `MPTAmount` | `makeBookStepIm`[^make-bookstepim] | +| BookStepIM (offer crossing) | `BookOfferCrossingStep` | `IOUAmount` | `MPTAmount` | `makeBookStepIm` | | XRPEndpoint (payment) | `XRPEndpointPaymentStep`[^xrpendpointpaymentstep] | `XRPAmount` | `XRPAmount` | `make_XRPEndpointStep`[^make-xrpendpointstep] | | XRPEndpoint (offer crossing) | `XRPEndpointOfferCrossingStep`[^xrpendpointoffercrossingstep] | `XRPAmount` | `XRPAmount` | `make_XRPEndpointStep` | | MPTEndpoint (payment) | `MPTEndpointPaymentStep`[^mptendpointpaymentstep] | `MPTAmount` | `MPTAmount` | `make_MPTEndpointStep`[^make-mptendpointstep] | | MPTEndpoint (offer crossing) | `MPTEndpointOfferCrossingStep`[^mptendpointoffercrossingstep] | `MPTAmount` | `MPTAmount` | `make_MPTEndpointStep` | -[^directipaymentstep]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L215-L256) -[^directioffercrossingstep]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L259-L307) -[^bookpaymentstep]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L250-L339) -[^bookoffercrossingstep]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L343-L537) -[^xrpendpointpaymentstep]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L152-L169) -[^xrpendpointoffercrossingstep]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L172-L225) -[^mptendpointpaymentstep]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L247-L278) -[^mptendpointoffercrossingstep]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L281-L316) -[^iouamount]: [`IOUAmount.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/IOUAmount.h#L25-L88) -[^xrpamount]: [`XRPAmount.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/XRPAmount.h#L20-L292) -[^mptamount]: [`MPTAmount.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/MPTAmount.h#L17-L169) -[^make-directstepi]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L950-L977) -[^make-bookstepii]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1498-L1502) -[^make-bookstepix]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1504-L1508) -[^make-bookstepxi]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1510-L1514) -[^make-bookstepmm]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1517-L1524) -[^make-bookstepmx]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1538-L1542) -[^make-bookstepxm]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1544-L1548) -[^make-bookstepmi]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1526-L1530) -[^make-bookstepim]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1532-L1536) -[^make-xrpendpointstep]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L390-L412) -[^make-mptendpointstep]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L958-L985) +[^directipaymentstep]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L231-L279) +[^directioffercrossingstep]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L282-L337) +[^bookpaymentstep]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L282-L369) +[^bookoffercrossingstep]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L373-L557) +[^xrpendpointpaymentstep]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L167-L187) +[^xrpendpointoffercrossingstep]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L190-L241) +[^mptendpointpaymentstep]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L242-L281) +[^mptendpointoffercrossingstep]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L284-L326) +[^iouamount]: [`IOUAmount.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/IOUAmount.h#L24-L91) +[^xrpamount]: [`XRPAmount.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/XRPAmount.h#L19-L237) +[^mptamount]: [`MPTAmount.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/MPTAmount.h#L16-L83) +[^make-directstepi]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L922-L947) +[^make-bookstepii]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1521-L1525) +[^make-bookstepix]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1527-L1531) +[^make-bookstepxi]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1533-L1537) +[^make-bookstepmm]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1540-L1544) +[^make-bookstepmx]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1558-L1562) +[^make-bookstepxm]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1564-L1568) +[^make-bookstepmi]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1546-L1550) +[^make-bookstepim]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1552-L1556) +[^make-xrpendpointstep]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L394-L415) +[^make-mptendpointstep]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L911-L936) ## 1.2. Class Relationships @@ -222,7 +222,7 @@ Given a requested output amount, this function[^directstepi-revimp]: 2. Calculates quality adjustments from both source and destination accounts via [`qualities`](#213-quality-functions) which calls [`quality`](#221-quality-implementation) (payment - respects QualityIn/QualityOut in certain conditions) or [`quality`](#231-quality-implementation) (offer crossing - always returns QUALITY_ONE) 3. Computes how much must flow on the trust line to produce the desired output, accounting for destination quality 4. Calculates the required input amount, accounting for source quality and transfer fees -5. Makes the payment by calling `rippleCredit` to update the trust line in the sandbox +5. Makes the payment by calling `directSendNoFee` to update the trust line in the sandbox 6. Returns the actual input required and output delivered #### 2.1.1.1. `revImp` Pseudo-Code @@ -238,7 +238,7 @@ def revImp(sb, out: IOUAmount) -> (in: IOUAmount, out: IOUAmount): # dstQIn stands for Destination Quality In # - the quality the destination applies for receiving funds # Each quality can be a discount or a premium. - srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::reverse) + srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::Reverse) # Set the IOU to currency and correct issuer pair srcToDstIss.currency = currency_ @@ -259,7 +259,7 @@ def revImp(sb, out: IOUAmount) -> (in: IOUAmount, out: IOUAmount): in = roundUp(srcToDst * srcQOut / QUALITY_ONE) cache_ = { in: in, srcToDst: srcToDst, out: out, srcDebtDir} # Update the trustline with appropriate srcToDst amount - rippleCredit(sb, src_, dst_, srcToDstIss(srcToDst)) + directSendNoFee(sb, src_, dst_, srcToDstIss(srcToDst)) return { in: in, out: out} # We don't have enough liquidity, so we will require as much input as will produce maximum output we can have @@ -267,11 +267,11 @@ def revImp(sb, out: IOUAmount) -> (in: IOUAmount, out: IOUAmount): actualOut = roundDown(maxSrcToDst * dstQIn / QUALITY_ONE) cache_ = { in: in, srcToDst: maxSrcToDst, out: actualOut, srcDebtDir} # Update the trustline with the amount limited to liquidity of this step - rippleCredit(sb, src_, dst_, srcToDstIss(maxSrcToDst)) + directSendNoFee(sb, src_, dst_, srcToDstIss(maxSrcToDst)) return { in: in, out: actualOut} ``` -[^directstepi-revimp]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L487-L565) +[^directstepi-revimp]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L503-L567) ### 2.1.2. `fwdImp` Implementation @@ -289,7 +289,7 @@ def fwdImp(sb, in: IOUAmount) -> (in: IOUAmount, out: IOUAmount): # Recalculate liquidity based on cached srcToDst from reverse pass maxSrcToDst, srcDebtDir = maxFlow(sb, cache_.srcToDst) - srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::forward) + srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::Forward) srcToDstIss.currency = currency_ srcToDstIss.account = dst_ if srcDebtDir == 'redeems' else src_ @@ -308,20 +308,20 @@ def fwdImp(sb, in: IOUAmount) -> (in: IOUAmount, out: IOUAmount): # changed from rev to fwd passes. setCacheLimiting(in, srcToDst, out, srcDebtDir) # Make the payment using the cached srcToDst value - rippleCredit(sb, src_, dst_, srcToDstIss(cache_.srcToDst)) + directSendNoFee(sb, src_, dst_, srcToDstIss(cache_.srcToDst)) else: # We don't have full liquidity actualIn = roundUp(maxSrcToDst * srcQOut / QUALITY_ONE) out = roundDown(maxSrcToDst * dstQIn / QUALITY_ONE) setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir) # Make the payment using the cached srcToDst value - rippleCredit(sb, src_, dst_, srcToDstIss(cache_.srcToDst)) + directSendNoFee(sb, src_, dst_, srcToDstIss(cache_.srcToDst)) # This ensures that the forward pass never returns better rates than reverse pass promised return { in: cache_.in, out: cache_.out} ``` -[^directstepi-fwdimp]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L615-L691) +[^directstepi-fwdimp]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L617-L682) ### 2.1.3. Quality Functions @@ -334,14 +334,14 @@ It returns two quality values: These qualities are used to calculate how input amounts transform to output amounts accounting for transfer fees and quality adjustments. The `strandDir` (strand direction) parameter controls whether the previous step's debt direction is fetched from cache or calculated fresh: -- `StrandDirection::reverse` (reverse pass): Calculate previous step's debt direction from current ledger state -- `StrandDirection::forward` (forward pass): Use cached debt direction from the reverse pass +- `StrandDirection::Reverse` (reverse pass): Calculate previous step's debt direction from current ledger state +- `StrandDirection::Forward` (forward pass): Use cached debt direction from the reverse pass The `qualities` function delegates to two specialized helpers based on the current step's debt direction: **`qualitiesSrcRedeems()`**[^directstepi-qualitiessrcredeems]: Called when the source is redeeming (holder sending to issuer) -[^directstepi-qualitiessrcredeems]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L750-L762) +[^directstepi-qualitiessrcredeems]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L738-L749) When a holder returns IOUs to their issuer: @@ -352,7 +352,7 @@ Quality must not improve as value flows through the path. If the previous step h **`qualitiesSrcIssues(prevStepDebtDirection)`**[^directstepi-qualitiessrcissues]: Called when the source is issuing (issuer sending to holder) -[^directstepi-qualitiessrcissues]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L767-L788) +[^directstepi-qualitiessrcissues]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L754-L771) When an issuer sends IOUs to a holder: @@ -381,7 +381,7 @@ def qualitiesSrcRedeems(sb) -> (srcQOut, dstQIn): return {QUALITY_ONE, QUALITY_ONE} prevStepQIn = prevStep_.lineQualityIn(sb) - srcQOut = quality(sb, QualityDirection::out) + srcQOut = quality(sb, QualityDirection::Out) # Use the worse of the two qualities (higher value = worse for sender) if prevStepQIn > srcQOut: @@ -396,7 +396,7 @@ def qualitiesSrcIssues(sb, prevStepDebtDirection) -> (srcQOut, dstQIn): else: srcQOut = QUALITY_ONE - dstQIn = quality(sb, QualityDirection::in) + dstQIn = quality(sb, QualityDirection::In) # If this is the last step, cap destination quality at QUALITY_ONE if isLast_ and dstQIn > QUALITY_ONE: @@ -405,7 +405,7 @@ def qualitiesSrcIssues(sb, prevStepDebtDirection) -> (srcQOut, dstQIn): return {srcQOut, dstQIn} ``` -[^directstepi-qualities]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L793-L811) +[^directstepi-qualities]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L776-L792) ### 2.1.4. `qualityUpperBound` Implementation @@ -420,7 +420,7 @@ The implementation is the same for both `DirectIPaymentStep` and `DirectIOfferCr ```python def qualityUpperBound(v: ReadView, prevStepDir: DebtDirection) -> (q: Quality, dir: DebtDirection): # Determine debt direction for this step - dir = debtDirection(v, StrandDirection::forward) + dir = debtDirection(v, StrandDirection::Forward) # Calculate qualities based on whether source is redeeming or issuing if dir == "redeems": @@ -438,7 +438,7 @@ def qualityUpperBound(v: ReadView, prevStepDir: DebtDirection) -> (q: Quality, d return {q, dir} ``` -[^directstepi-qualityupperbound]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L823-L842) +[^directstepi-qualityupperbound]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L804-L819) ### 2.1.5. `check` Implementation @@ -452,18 +452,18 @@ The base class `DirectStepI` provides[^directstepi-check] common validation that - `tecINTERNAL`[^check-tecinternal]: AMM account exists but AMM ledger entry does not exist (should not happen) - `terNO_LINE`[^check-no-ripple-line]: The previous step is also a DirectStep (two consecutive DirectSteps). `checkNoRipple` reads the two trust lines that connect to the intermediate account (the source of the current step): one from the source of the previous step and one to the destination of the current step. This check fails if either trust line does not exist. - `terNO_RIPPLE`[^check-no-ripple]: The previous step is also a DirectStep and the intermediate account has NoRipple set on **both** trust lines. If either trust line has NoRipple cleared on the intermediate account's side, rippling is allowed and this check passes. -- `temBAD_PATH_LOOP`[^check-bad-path-loop-book][^check-bad-path-loop-seen]: Path loop detected (same issue appears multiple times in invalid pattern) - -[^check-bad-path-null]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L852) -[^check-bad-path-same]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L858) -[^check-no-account]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L867) -[^check-freeze]: [`StepChecks.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StepChecks.h#L27) (also lines 35, 41, 57) - called from [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L873) -[^check-tecinternal]: [`StepChecks.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StepChecks.h#L52) - called from [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L873) -[^check-no-ripple-line]: [`StepChecks.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StepChecks.h#L80) - called from [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L885) -[^check-no-ripple]: [`StepChecks.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StepChecks.h#L88) - called from [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L885) -[^check-bad-path-loop-book]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L911) -[^check-bad-path-loop-seen]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L921) -[^directstepi-check]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L846-L926) +- `temBAD_PATH_LOOP`[^check-bad-path-loop-book][^check-bad-path-loop-seen]: Path loop detected (the asset already appears in the `seenDirectAssets` loop-detection set for this direction) + +[^check-bad-path-null]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L829) +[^check-bad-path-same]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L835) +[^check-no-account]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L842) +[^check-freeze]: [`StepChecks.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StepChecks.h#L26) (also lines 34, 40, 55) - called from [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L848) +[^check-tecinternal]: [`StepChecks.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StepChecks.h#L51) - called from [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L848) +[^check-no-ripple-line]: [`StepChecks.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StepChecks.h#L78) - called from [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L859) +[^check-no-ripple]: [`StepChecks.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StepChecks.h#L86) - called from [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L859) +[^check-bad-path-loop-book]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L885) +[^check-bad-path-loop-seen]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L894) +[^directstepi-check]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L823-L899) ## 2.2. DirectIPaymentStep (Payment-Specific Implementation) @@ -485,7 +485,7 @@ def quality(sb, qDir: QualityDirection) -> int: return QUALITY_ONE # Determine which quality field to read based on direction and account ordering - if qDir == QualityDirection::in: + if qDir == QualityDirection::In: # Destination quality in field = sfLowQualityIn if dst_ < src_ else sfHighQualityIn else: @@ -502,13 +502,13 @@ def quality(sb, qDir: QualityDirection) -> int: return q ``` -[^directipaymentstep-quality]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L312-L348) +[^directipaymentstep-quality]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L342-L380) ### 2.2.2. `maxFlow` Implementation The `maxFlow`[^directipaymentstep-maxflow] method determines the maximum amount of IOU tokens that can flow from source to destination on this trust line, which `revImp` uses to constrain its calculations. The method also returns the debt direction, indicating whether the source is issuing or redeeming IOUs. -[^directipaymentstep-maxflow]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L359-L362) (calls [`maxPaymentFlow`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L457-L469)) +[^directipaymentstep-maxflow]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L391-L394) (calls [`maxPaymentFlow`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L478-L488)) In DirectIPaymentStep, `maxFlow` returns: @@ -521,7 +521,7 @@ In DirectIPaymentStep, `maxFlow` returns: # The out parameter is unused def maxFlow(sb, out) -> (maxSrcToDst, srcDebtDir): # Get the balance of src_ on the trustline - srcOwed = accountHolds(sb, src_, currency_, dst_, fhIGNORE_FREEZE) + srcOwed = accountHolds(sb, src_, currency_, dst_, FreezeHandling::IgnoreFreeze) if srcOwed > 0: # Holder is sending to issuer, they can only send as much as was issued to them @@ -542,11 +542,11 @@ The `check` method[^directipaymentstep-check] validates payment-specific constra - `terNO_RIPPLE`[^check-payment-no-ripple]: Previous step was a BookStep and the trust line has NoRipple flag set - `tecPATH_DRY`[^check-payment-path-dry]: Trust line has no liquidity available (at limit) -[^check-payment-no-line]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L398) -[^check-payment-no-auth]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L410) -[^check-payment-no-ripple]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L421) -[^check-payment-path-dry]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L435) -[^directipaymentstep-check]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L387-L440) +[^check-payment-no-line]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L427) +[^check-payment-no-auth]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L437) +[^check-payment-no-ripple]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L445) +[^check-payment-path-dry]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L458) +[^directipaymentstep-check]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L418-L463) ## 2.3. DirectIOfferCrossingStep (Offer Crossing-Specific Implementation) @@ -554,12 +554,12 @@ The `check` method[^directipaymentstep-check] validates payment-specific constra This function[^directioffercrossingstep-quality] always returns `QUALITY_ONE`, ignoring trust line quality settings. -[^directioffercrossingstep-quality]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L351-L356) +[^directioffercrossingstep-quality]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L383-L388) ### 2.3.2. `maxFlow` Implementation If this is the last step, `maxFlow`[^directioffercrossingstep-maxflow] returns the desired amount, allowing unlimited flow to destination. This effectively ignores trust line limits during offer crossing. -[^directioffercrossingstep-maxflow]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L365-L384) +[^directioffercrossingstep-maxflow]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L397-L415) The system presumes that when someone creates an offer, they intend to fill as much of that offer as possible, even if it exceeds the trust line limit. Trust line limits exist to protect against unwanted incoming tokens, but creating an offer is an explicit expression of intent to receive those tokens, so the limit restriction is waived. @@ -568,13 +568,13 @@ Otherwise, it returns using the same logic as DirectIPaymentStep. ### 2.3.3. `check` Implementation The function[^directioffercrossingstep-check] has no additional failure conditions beyond the [base class checks](#215-check-implementation). Offer crossing does not require a pre-existing trust line for `takerPays`, but placing the offer would fail if `takerGets` trust line did not exist for the holder. -[^directioffercrossingstep-check]: [`DirectStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/DirectStep.cpp#L443-L451) +[^directioffercrossingstep-check]: [`DirectStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/DirectStep.cpp#L466-L472) # 3. XRPEndpointStep ## 3.1. `revImp` Implementation -[^xrpendpointstep-revimp]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L251-L269) +[^xrpendpointstep-revimp]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L261-L279) `XRPEndpointStep::revImp`[^xrpendpointstep-revimp] transfers XRP from or to the user account. Uses `xrpAccount()` (a zero-valued sentinel account) as a bookkeeping marker to indicate where XRP enters or exits the payment flow - it's not a real account. If this is the last step (destination), accepts all requested XRP. If this is the first step (source), limits transfer to available balance after accounting for reserves. For offer crossing, when this is the first step (XRP source) and the trust line or MPToken for the delivered asset does not yet exist, reduces owner count by 1 when calculating available balance. @@ -595,7 +595,7 @@ def revImp(sb, afView, ofrsToRm, out: XRPAmount) -> (in: XRPAmount, out: XRPAmou sender = xrpAccount() if isLast_ else acc_ receiver = acc_ if isLast_ else xrpAccount() - ter = accountSend(sb, sender, receiver, result) + ter = accountSend(sb, sender, receiver, toSTAmount(result)) if ter != tesSUCCESS: return {in: 0, out: 0} @@ -606,7 +606,7 @@ def revImp(sb, afView, ofrsToRm, out: XRPAmount) -> (in: XRPAmount, out: XRPAmou ## 3.2. `fwdImp` Implementation -[^xrpendpointstep-fwdimp]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L273-L292) +[^xrpendpointstep-fwdimp]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L283-L302) `XRPEndpointStep::fwdImp`[^xrpendpointstep-fwdimp] has the same logic as `revImp` but starts from the input amount. @@ -635,7 +635,7 @@ def fwdImp(sb, afView, ofrsToRm, inAmount: XRPAmount) -> (in: XRPAmount, out: XR ## 3.3. `qualityUpperBound` Implementation -[^xrpendpointstep-qualityupperbound]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L240-L247) +[^xrpendpointstep-qualityupperbound]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L254-L257) `XRPEndpointStep::qualityUpperBound`[^xrpendpointstep-qualityupperbound] always returns quality of `QUALITY_ONE` (1:1) and "issues" direction. @@ -643,45 +643,45 @@ def fwdImp(sb, afView, ofrsToRm, inAmount: XRPAmount) -> (in: XRPAmount, out: XR ```python def qualityUpperBound(prevStepDir: DebtDirection) -> (q: Quality, dir: DebtDirection): - return {QUALITY_ONE, "issues") + return {QUALITY_ONE, "issues"} ``` ## 3.4. `check` Implementation -[^xrpendpointstep-check]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L333-L370) +[^xrpendpointstep-check]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L338-L375) -`XRPEndpointStep::check`[^xrpendpointstep-check] verifies the account exists, ensures the step is at a strand boundary (first or last position), checks for XRP freeze status, and detects path loops. Can return one of the following errors: +`XRPEndpointStep::check`[^xrpendpointstep-check] verifies the account exists, ensures the step is at a strand boundary (first or last position), checks for an account/trust-line freeze, and detects path loops. Can return one of the following errors: - `temBAD_PATH`[^xrpcheck-bad-path-null][^xrpcheck-bad-path-boundary]: Account is null or step is not first or last in strand (XRP endpoints must be at strand boundaries) - `terNO_ACCOUNT`[^xrpcheck-no-account]: Account does not exist -- `terNO_LINE`[^xrpcheck-freeze]: XRP currency is frozen -- `temBAD_PATH_LOOP`[^xrpcheck-bad-path-loop]: Path loop detected (XRP issue appears multiple times in invalid pattern) +- `terNO_LINE`[^xrpcheck-freeze]: a freeze applies to the endpoint account (global freeze, directional/deep trust-line freeze, or LP-token freeze), as determined by `checkFreeze` +- `temBAD_PATH_LOOP`[^xrpcheck-bad-path-loop]: Path loop detected (the XRP asset already appears in the `seenDirectAssets` loop-detection set for this direction) -[^xrpcheck-bad-path-null]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L338) -[^xrpcheck-bad-path-boundary]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L352) -[^xrpcheck-no-account]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L347) -[^xrpcheck-freeze]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L357) - calls [`StepChecks.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/StepChecks.h#L27) -[^xrpcheck-bad-path-loop]: [`XRPEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/XRPEndpointStep.cpp#L366) +[^xrpcheck-bad-path-null]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L343) +[^xrpcheck-bad-path-boundary]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L357) +[^xrpcheck-no-account]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L352) +[^xrpcheck-freeze]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L362) - calls [`StepChecks.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/detail/StepChecks.h#L26) +[^xrpcheck-bad-path-loop]: [`XRPEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/XRPEndpointStep.cpp#L371) # 4. MPTEndpointStep MPTEndpointStep handles Multi-Purpose Token (MPT) transfers at the source or destination of a payment strand. Like XRPEndpointStep, it can only appear at strand boundaries (first or last step)[^mpt-boundary-check]. Unlike XRP which has no issuer, each MPTEndpointStep must have exactly one endpoint be the issuer[^mpt-issuer-check] - either the source or destination must be the issuer account. This means individual steps either issue (issuer -> holder) or redeem (holder -> issuer)[^mpt-debt-direction], which determines whether transfer fees apply and affects authorization checks. -[^mpt-boundary-check]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L928-L932) -[^mpt-issuer-check]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L935-L940) -[^mpt-debt-direction]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L480-L488) +[^mpt-boundary-check]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L886-L890) +[^mpt-issuer-check]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L893-L897) +[^mpt-debt-direction]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L459-L464) For holder-to-holder payments, the payment path contains two MPTEndpointSteps: the first step redeems from holder to issuer, and the last step issues from issuer to holder. Transfer fees are applied during the issuing step (when `qualitiesSrcIssues` sees that the previous step redeemed). See [Direct MPT Payment Execution](../payments/README.md#43-mpt-payment-execution) for detailed holder-to-holder transfer mechanics. -For non-issuer accounts, authorization is validated via `requireAuth`[^mpt-require-auth], which checks the [`lsfMPTRequireAuth`](../mpts/README.md#2121-flags) flag on the issuance and the [`lsfMPTAuthorized`](../mpts/README.md#2221-flags) flag on the holder's MPToken. When the issuance has a [DomainID](../mpts/README.md#11-domainid-and-authorization) set, credentials are validated against that domain. DEX operations validate permissions via [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed), which verifies the [`lsfMPTCanTrade`](../mpts/README.md#2121-flags) flag for DEX usage and checks both issuance-level and holder-level lock flags. +For non-issuer accounts, authorization is validated via `requireAuth`[^mpt-require-auth], which checks the [`lsfMPTRequireAuth`](../mpts/README.md#2121-flags) flag on the issuance and the [`lsfMPTAuthorized`](../mpts/README.md#2221-flags) flag on the holder's MPToken. When the issuance has a [DomainID](../mpts/README.md#11-domainid-and-authorization) set, credentials are validated against that domain. DEX operations validate the [`lsfMPTCanTrade`](../mpts/README.md#2121-flags) flag via [`canTrade`](../mpts/README.md#361-cantrade); issuance- and holder-level lock flags are checked separately by `isGlobalFrozen` and `isIndividualFrozen`. -[^mpt-require-auth]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L333-L342) +[^mpt-require-auth]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L342-L349) ## 4.1. `revImp` Implementation -[^mptendpointstep-revimp]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L493-L583) +[^mptendpointstep-revimp]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L469-L549) -`MPTEndpointStep::revImp`[^mptendpointstep-revimp] starts by determining the maximum flow by calling `accountHolds`, which returns different values depending on the source: for holders, it reads the `MPTAmount` from the holder's MPToken entry; for the issuer, it calculates available capacity as `MaximumAmount - OutstandingAmount`. The `accountHolds` call uses `fhIGNORE_FREEZE` and `ahIGNORE_AUTH` flags to skip freeze and authorization checks, since those validations are performed separately in the `check()` method during path validation. +`MPTEndpointStep::revImp`[^mptendpointstep-revimp] starts by determining the maximum flow by calling `accountFunds`, which returns different values depending on the source: for holders, it reads the `MPTAmount` from the holder's MPToken entry; for the issuer, it calculates available capacity as `MaximumAmount - OutstandingAmount`. The `accountFunds` call uses `FreezeHandling::IgnoreFreeze` and `AuthHandling::IgnoreAuth` flags to skip freeze and authorization checks, since those validations are performed separately in the `check()` method during path validation. When a holder is the source (redeeming), flow is limited by the holder's balance. When the issuer is the source, the behavior depends on whether this is a direct payment or part of a multi-step path. For direct issuer-to-holder payments (no previous step, such as when the issuer sends MPT directly to a holder without path finding or currency conversion), flow is limited by available capacity (`MaximumAmount - OutstandingAmount`). For multi-step paths where the issuer is the last step, the implementation cannot determine the actual capacity limit yet because the previous step's direction (issuing or redeeming) affects the net change to `OutstandingAmount`. In holder-to-holder transfers, the previous step redeems (decreasing `OutstandingAmount`) before this step issues (increasing it), so the net effect may be zero or even a decrease despite this step appearing to "issue" tokens. Therefore, when there's a previous step, `maxPaymentFlow` returns `MaximumAmount` to avoid prematurely limiting flow, letting the previous step determine the actual constraint. @@ -689,7 +689,7 @@ When a holder is the source (redeeming), flow is limited by the holder's balance 1. Step 1 (first): Holder A -> Issuer (redeems 100, will decrease `OutstandingAmount` to 850) 2. Step 2 (last): Issuer -> Holder B (issues 100, will increase `OutstandingAmount` to 950) -During the reverse pass, Step 2 is evaluated first. At this point, `accountHolds` for the issuer returns `MaximumAmount - OutstandingAmount = 50`, suggesting only 50 MPT can flow. However, this is incorrect because Step 1 hasn't been evaluated yet. Once Step 1 redeems 100 MPT, there will be sufficient capacity. By returning `MaximumAmount = 1000` from `maxPaymentFlow`, Step 2 doesn't artificially limit the flow, and Step 1's balance (which limits to 100) becomes the actual constraint. +During the reverse pass, Step 2 is evaluated first. At this point, `accountFunds` for the issuer returns `MaximumAmount - OutstandingAmount = 50`, suggesting only 50 MPT can flow. However, this is incorrect because Step 1 hasn't been evaluated yet. Once Step 1 redeems 100 MPT, there will be sufficient capacity. By returning `MaximumAmount = 1000` from `maxPaymentFlow`, Step 2 doesn't artificially limit the flow, and Step 1's balance (which limits to 100) becomes the actual constraint. Transfer fees apply when the issuer issues and the previous step redeems. @@ -703,7 +703,7 @@ def revImp(sb, afView, ofrsToRm, out: MPTAmount) -> (in: MPTAmount, out: MPTAmou # Determine max flow and direction maxSrcToDst, srcDebtDir = maxPaymentFlow(sb) - srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::reverse) + srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::Reverse) if maxSrcToDst <= 0: resetCache(srcDebtDir) @@ -721,7 +721,7 @@ def revImp(sb, afView, ofrsToRm, out: MPTAmount) -> (in: MPTAmount, out: MPTAmou in = roundUp(srcToDst * srcQOut / QUALITY_ONE) cache_ = {in, srcToDst, srcToDst, srcDebtDir} - ter = rippleCredit(sb, src_, dst_, srcToDst, checkIssuer=False) + ter = directSendNoFee(sb, src_, dst_, srcToDst, checkIssuer=False) if ter != tesSUCCESS: resetCache(srcDebtDir) return {0, 0} @@ -732,7 +732,7 @@ def revImp(sb, afView, ofrsToRm, out: MPTAmount) -> (in: MPTAmount, out: MPTAmou actualOut = maxSrcToDst cache_ = {in, maxSrcToDst, actualOut, srcDebtDir} - ter = rippleCredit(sb, src_, dst_, maxSrcToDst, checkIssuer=False) + ter = directSendNoFee(sb, src_, dst_, maxSrcToDst, checkIssuer=False) if ter != tesSUCCESS: resetCache(srcDebtDir) return {0, 0} @@ -743,7 +743,7 @@ def revImp(sb, afView, ofrsToRm, out: MPTAmount) -> (in: MPTAmount, out: MPTAmou ```python def maxPaymentFlow(sb: ReadView) -> (MPTAmount, DebtDirection): - maxFlow = accountHolds(src_, mptIssue_, fhIGNORE_FREEZE, ahIGNORE_AUTH) + maxFlow = accountFunds(src_, mptIssue_, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth) # Holder to issuer (redeeming) if src_ != issuer: @@ -758,7 +758,7 @@ def maxPaymentFlow(sb: ReadView) -> (MPTAmount, DebtDirection): # Last step with issuer as source # Allow temporary overflow - previous step limits flow - maxAmount = sle[sfMaximumAmount] or maxMPTokenAmount + maxAmount = sle[sfMaximumAmount] or kMaxMpTokenAmount return {maxAmount, "issues"} return {0, "issues"} @@ -766,7 +766,7 @@ def maxPaymentFlow(sb: ReadView) -> (MPTAmount, DebtDirection): ## 4.2. `fwdImp` Implementation -[^mptendpointstep-fwdimp]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L632-L722) +[^mptendpointstep-fwdimp]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L599-L681) `MPTEndpointStep::fwdImp`[^mptendpointstep-fwdimp] processes the forward pass using cached state from reverse to ensure consistency. @@ -777,7 +777,7 @@ def fwdImp(sb, afView, ofrsToRm, in: MPTAmount) -> (in: MPTAmount, out: MPTAmoun assert cache_ is not None maxSrcToDst, srcDebtDir = maxPaymentFlow(sb) - srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::forward) + srcQOut, dstQIn = qualities(sb, srcDebtDir, StrandDirection::Forward) if maxSrcToDst <= 0: resetCache(srcDebtDir) @@ -793,7 +793,7 @@ def fwdImp(sb, afView, ofrsToRm, in: MPTAmount) -> (in: MPTAmount, out: MPTAmoun setCacheLimiting(in, srcToDst, out, srcDebtDir) # Transfer MPT using cached amount from reverse pass - ter = rippleCredit(sb, src_, dst_, toSTAmount(cache_.srcToDst, mptIssue_), checkIssuer=False) + ter = directSendNoFee(sb, src_, dst_, toSTAmount(cache_.srcToDst, mptIssue_), checkIssuer=False) if ter != tesSUCCESS: resetCache(srcDebtDir) return {0, 0} @@ -803,7 +803,7 @@ def fwdImp(sb, afView, ofrsToRm, in: MPTAmount) -> (in: MPTAmount, out: MPTAmoun setCacheLimiting(actualIn, maxSrcToDst, out, srcDebtDir) # Transfer MPT using cached amount from reverse pass - ter = rippleCredit(sb, src_, dst_, toSTAmount(cache_.srcToDst, mptIssue_), checkIssuer=False) + ter = directSendNoFee(sb, src_, dst_, toSTAmount(cache_.srcToDst, mptIssue_), checkIssuer=False) if ter != tesSUCCESS: resetCache(srcDebtDir) return {0, 0} @@ -813,7 +813,7 @@ def fwdImp(sb, afView, ofrsToRm, in: MPTAmount) -> (in: MPTAmount, out: MPTAmoun ## 4.3. `qualityUpperBound` Implementation -[^mptendpointstep-qualityupperbound]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L851-L872) +[^mptendpointstep-qualityupperbound]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L801-L817) `MPTEndpointStep::qualityUpperBound`[^mptendpointstep-qualityupperbound] calculates quality as `srcQOut / dstQIn`, representing the exchange rate (how much input is needed per unit of output). Since MPTs have no trust-line-level quality adjustments, `dstQIn` is always `QUALITY_ONE`, so the quality simplifies to `srcQOut`. @@ -830,7 +830,7 @@ Transfer fees only apply when tokens move between holders through the issuer as ```python def qualityUpperBound(sb, prevStepDir: DebtDirection) -> (Quality, DebtDirection): - dir = debtDirection(sb, StrandDirection::forward) + dir = debtDirection(sb, StrandDirection::Forward) if dir == "redeems": srcQOut, dstQIn = qualitiesSrcRedeems(sb) @@ -871,30 +871,33 @@ def qualitiesSrcIssues(prevStepDebtDirection: DebtDirection) -> (uint32, uint32) ## 4.4. `check` Implementation -[^mptendpointstep-check]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L876-L943) +[^mptendpointstep-check]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L821-L900) **Common checks[^mptendpointstep-check] (both payment and offer crossing):** -[^mptendpointstep-check-bad-path-null]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L879-L883) -[^mptendpointstep-check-bad-path-same]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L885-L889) -[^mptendpointstep-check-no-account]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L891-L898) -[^mptendpointstep-check-bad-path-loop]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L919-L925) -[^mptendpointstep-check-bad-path-boundary]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L927-L932) -[^mptendpointstep-check-bad-path-issuer]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L934-L940) +[^mptendpointstep-check-bad-path-null]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L824-L828) +[^mptendpointstep-check-bad-path-same]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L830-L834) +[^mptendpointstep-check-no-account]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L836-L841) +[^mptendpointstep-check-bad-path-loop]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L877-L883) +[^mptendpointstep-check-bad-path-boundary]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L885-L890) +[^mptendpointstep-check-bad-path-issuer]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L892-L897) +[^mpt-common-freeze]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L849-L856) +[^mpt-common-seenbookouts]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L858-L873) - `temBAD_PATH`: - Source/destination is null, or source equals destination[^mptendpointstep-check-bad-path-null][^mptendpointstep-check-bad-path-same] - Step not at strand boundary (MPT endpoints must be first or last)[^mptendpointstep-check-bad-path-boundary] - Invalid src/dst (exactly one must be issuer)[^mptendpointstep-check-bad-path-issuer] - `terNO_ACCOUNT`[^mptendpointstep-check-no-account]: Source account does not exist -- `temBAD_PATH_LOOP`[^mptendpointstep-check-bad-path-loop]: MPT issue appears multiple times in the same role (source or destination) within a strand. Since MPTEndpointStep can only appear at strand boundaries, each strand tracks seen assets separately for source-side (`seenDirectAssets[0]`) and destination-side (`seenDirectAssets[1]`). If the same MPT issue appears twice on the same side, it would count the same issuer's liquidity pool multiple times, leading to incorrect flow calculations +- `terLOCKED`[^mpt-common-freeze]: the MPT is frozen at the endpoint account. A global freeze applies on the first step, or the holder's MPToken is individually frozen. This check is skipped when the step is both first and last (a pure issuer/holder issue or redeem). +- `temBAD_PATH_LOOP`[^mptendpointstep-check-bad-path-loop]: MPT issue appears multiple times in the same role (source or destination) within a strand. Since MPTEndpointStep can only appear at strand boundaries, each strand tracks seen assets separately for source-side (`seenDirectAssets[0]`) and destination-side (`seenDirectAssets[1]`). If the same MPT issue appears twice on the same side, it would count the same issuer's liquidity pool multiple times, leading to incorrect flow calculations. `temBAD_PATH_LOOP` is also returned if this MPT issue was already produced as a book step's output earlier in the strand, tracked in `seenBookOuts`[^mpt-common-seenbookouts] **Payment-specific (`MPTEndpointPaymentStep`):**[^mptendpointstep-check-payment] -[^mptendpointstep-check-payment]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L321-L401) -[^mptendpointstep-check-payment-requireauth]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L331-L343) -[^mptendpointstep-check-payment-frozen]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L354-L360) -[^mptendpointstep-check-payment-dex]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L368-L384) -[^mptendpointstep-check-payment-dry]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L391-L398) +[^mptendpointstep-check-payment]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L331-L393) +[^mptendpointstep-check-payment-requireauth]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L340-L350) +[^mptendpointstep-check-payment-frozen]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L361-L366) +[^mptendpointstep-check-payment-dex]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L374-L376) +[^mptendpointstep-check-payment-dry]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L383-L390) - Authorization[^mptendpointstep-check-payment-requireauth]: `requireAuth` for non-issuer src and dst - `tecOBJECT_NOT_FOUND`: Issuance doesn't exist @@ -905,22 +908,16 @@ def qualitiesSrcIssues(prevStepDebtDirection: DebtDirection) -> (uint32, uint32) - `tecLOCKED`: MPToken is frozen - `tecOBJECT_NOT_FOUND`: Issuance doesn't exist - `tecNO_AUTH`: Can't transfer between holders -- DEX crossing[^mptendpointstep-check-payment-dex]: [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed) when crossing order books - - `tecNO_ISSUER`: Issuer account doesn't exist +- DEX crossing[^mptendpointstep-check-payment-dex]: [`canTrade`](../mpts/README.md#361-cantrade) when crossing order books - `tecOBJECT_NOT_FOUND`: Issuance doesn't exist - - `tecLOCKED`: Issuance or holder MPToken is locked - - `tecNO_PERMISSION`: Can't trade on DEX or can't transfer -- `tecPATH_DRY`[^mptendpointstep-check-payment-dry]: Source balance is zero or negative (first step only) + - `tecNO_PERMISSION`: `lsfMPTCanTrade` flag not set +- `tecPATH_DRY`[^mptendpointstep-check-payment-dry]: Source balance is zero or negative; for an issuer source, no remaining issuance capacity (first step only) **Offer crossing-specific (`MPTEndpointOfferCrossingStep`):**[^mptendpointstep-check-offer] -[^mptendpointstep-check-offer]: [`MPTEndpointStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/MPTEndpointStep.cpp#L404-L418) +[^mptendpointstep-check-offer]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L396-L402) -- DEX authorization: [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed) for holder (if holder is not issuer) - - `tecNO_ISSUER`: Issuer account doesn't exist - - `tecOBJECT_NOT_FOUND`: Issuance doesn't exist - - `tecLOCKED`: Issuance or holder MPToken is locked - - `tecNO_PERMISSION`: Can't trade on DEX or can't transfer +- No additional MPT checks: offer crossing doesn't require a pre-existing MPToken, so it relies on the standard `MPTEndpointStep` checks (above) and returns `tesSUCCESS`. # 5. BookStep @@ -941,20 +938,20 @@ The reverse pass (`revImp`) works backwards from the desired output amount, iter **Implementation Architecture:** -In the actual implementation, both `revImp` and `fwdImp` create lambda functions called `eachOffer` that capture local variables (like `remainingOut`/`remainingIn`, `savedIns`, `savedOuts`, and `result`) by reference. Each method calls `forEachOffer()`, passing its lambda as the callback parameter. +In the actual implementation, both `revImp`[^revimp-eachoffer] and `fwdImp`[^fwdimp-eachoffer] create lambda functions called `eachOffer` that capture local variables (like `remainingOut`/`remainingIn`, `savedIns`, `savedOuts`, and `result`) by reference. Each method calls `forEachOffer()`, passing its lambda as the callback parameter. -[^revimp-eachoffer]: `revImp` lambda callback creation: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1039-L1086) -[^fwdimp-eachoffer]: `fwdImp` lambda callback creation: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1151-L1249) +[^revimp-eachoffer]: `revImp` lambda callback creation: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1019-L1064) +[^fwdimp-eachoffer]: `fwdImp` lambda callback creation: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1128-L1225) Inside `forEachOffer`, the first CLOB offer is fetched from the order book[^foreachoffer-traverse], then a single AMM synthetic offer is generated if the AMM's spot price can beat the CLOB quality[^foreachoffer-getammoffer] and processed first via the `eachOffer` callback. CLOB offers are then iterated one by one. For each offer (whether AMM or CLOB), `forEachOffer` calculates the offer amounts accounting for transfer fees and owner funding, then invokes the `eachOffer` callback with the calculated amounts. -[^foreachoffer-traverse]: Order book directory traversal via `FlowOfferStream`: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L707-L708) -[^foreachoffer-getammoffer]: AMM offer generation for each CLOB offer: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L852) +[^foreachoffer-traverse]: Order book directory traversal via `FlowOfferStream`: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L705) +[^foreachoffer-getammoffer]: AMM offer generation for each CLOB offer: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L831) The `eachOffer` callback first checks if `remainingOut <= 0` and returns `false` if the payment is already satisfied. Otherwise, it compares `stpAmt.out` (the amount that will flow through this step after accounting for transfer fees and owner funding)[^stpamt-calculation] against `remainingOut`. If the offer's output is less than or equal to the remaining output needed (`stpAmt.out <= remainingOut`), the offer is fully consumable - it consumes the offer and records its contribution to the step's total input and output, then returns `true` to continue (even if this offer satisfied the full amount, the callback returns `true` to ensure the offer is properly consumed; the next iteration will detect the zero remaining and stop). If the offer's output exceeds the remaining output needed (`stpAmt.out > remainingOut`), it calls `limitStepOut()` to adjust the amounts down to exactly what's needed, then partially consumes the offer and records the adjusted contribution, returning whether to continue based on whether the offer is fully consumed. When the callback returns `false`, `forEachOffer` stops iterating and returns control back to `revImp` or `fwdImp`. -[^stpamt-calculation]: Step amount calculation with transfer fees and owner funding: [`BookStep.cpp:776-804`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L776-L804) +[^stpamt-calculation]: Step amount calculation with transfer fees and owner funding: [`BookStep.cpp:769-792`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L769-L792) This callback architecture separates concerns: `forEachOffer` handles iteration mechanics - traversing the order book directory, generating AMM synthetic offers via `getAMMOffer`, comparing CLOB vs AMM quality to select the better source, performing asset-specific authorization checks, and verifying offer funding. The callback handles consumption decisions - comparing offer amounts against remaining needs and calling `limitStepOut` (adjusts offer amounts down when output exceeds what's needed) or `limitStepIn` (adjusts offer amounts down when input is limited) to scale offers appropriately. After the callback determines the amounts, it invokes `consumeOffer`, which performs the actual ledger updates: for CLOB offers, it adjusts the offer's TakerPays/TakerGets amounts and marks fully consumed offers for deletion; for AMM offers, it updates the AMM pool balances by transferring assets to/from the AMM account using `accountSend`. @@ -1004,36 +1001,39 @@ def forEachOffer(sb, callback): break +# Real signature: consumeOffer(sb, offer, ofrAmt, stepAmt, ownerGives); +# the overview passes a single stpAmt for brevity. def consumeOffer(sb, offer, stpAmt): - if offer.isCLOB(): - updateOfferAmounts(sb, offer, stpAmt) - else: - updateAMMPool(sb, offer, stpAmt) + # No isCLOB() branch: CLOB vs AMM is polymorphic, handled inside + # the offer.send() and offer.consume() methods. + offer.checkInvariant(stpAmt) # AMM pool-product invariant (fixAMMOverflowOffer) + offer.send(...) # two transfers: owner receives input, pays output (with fees) + offer.consume(sb, stpAmt) # CLOB: reduce/delete offer; AMM: update pool balances ``` **Offer Iteration and Funding:** The order book traversal in `forEachOffer` is implemented through `FlowOfferStream`[^flowofferstream-class], which extends `TOfferStreamBase`[^tofferstreambase-class] with permanent offer removal tracking. `TOfferStreamBase` contains a `BookTip` member and delegates directory iteration to it.[^offerstream-implementation] `BookTip` provides directory traversal sorted by quality, while `TOfferStreamBase` adds expiration checks, funding verification, and frozen asset handling. When processing each offer, `TOfferStreamBase::step()` calls `tip_.step()` to advance through the directory, then retrieves the offer entry via `tip_.entry()` and quality via `tip_.quality()`. -[^flowofferstream-class]: [`OfferStream.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/OfferStream.h#L131-L151) -[^tofferstreambase-class]: [`OfferStream.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/OfferStream.h#L17-L111) -[^offerstream-implementation]: `TOfferStreamBase` implementation with `BookTip` delegation: [`OfferStream.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/OfferStream.cpp#L184-L212) +[^flowofferstream-class]: [`OfferStream.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/OfferStream.h#L129-L149) +[^tofferstreambase-class]: [`OfferStream.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/OfferStream.h#L15-L109) +[^offerstream-implementation]: `TOfferStreamBase` implementation with `BookTip` delegation: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/OfferStream.cpp#L205-L232) `FlowOfferStream` adds the `permToRemove` collection, which tracks offers that should be permanently removed even if the strand is not applied. This is used by `forEachOffer` to track self-crossed offers and other invalid offers that need removal regardless of transaction outcome. During iteration, `TOfferStreamBase::step()` determines which offers to remove from the order book.[^offerstream-step] It marks offers for permanent removal when the ledger entry is missing, the offer has expired, either amount is zero, the asset is deep frozen, the offer owner's account is no longer in the offer's domain (for domain-restricted offers), or the owner has zero balance. For unfunded offers and tiny offers with reduced quality under `fixReducedOffersV1`, it distinguishes between offers that were already in that state versus offers that became that way during the current transaction by comparing balances in the current view against the pristine `cancelView`. Only offers that were already unfunded or tiny are permanently removed from the ledger; offers that became unfunded or tiny during execution are simply skipped for this transaction but remain in the order book. -[^offerstream-step]: Offer removal logic in `TOfferStreamBase::step()`: [`OfferStream.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/OfferStream.cpp#L193-L320) +[^offerstream-step]: Offer removal logic in `TOfferStreamBase::step()`: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/OfferStream.cpp#L214-L327) For each valid offer that passes these checks, `TOfferStreamBase` verifies whether the offer owner has sufficient balance to cover their takerGets obligation.[^offerstream-funds-helper] For IOU issuers, this returns the full requested amount directly since they can issue unlimited amounts. For MPT issuers, this returns the available minting capacity (`MaximumAmount - OutstandingAmount`) via `issuerFundsToSelfIssue`; if this amount is zero or negative (MaximumAmount reached), the offer is treated as unfunded and removed. For non-issuers, `TOfferStreamBase` queries the owner's actual available balance through `accountHolds` and stores it in the `ownerFunds_` member. -[^offerstream-funds-helper]: Owner funds verification via `accountFundsHelper`: [`OfferStream.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/OfferStream.cpp#L83-L109) +[^offerstream-funds-helper]: Owner funds verification via `accountFundsHelper`: [`OfferStream.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/OfferStream.cpp#L104-L130) -If the owner's balance is less than takerGets, the offer is underfunded but can still partially fill. The `TOfferStreamBase::ownerFunds()` method returns this actual available balance, which `forEachOffer` uses to proportionally adjust the offer amounts via `Quality::ceil_out()`. +If the owner's balance is less than takerGets, the offer is underfunded but can still partially fill. The `TOfferStreamBase::ownerFunds()` method returns this actual available balance, which `forEachOffer` uses to proportionally adjust the offer amounts via `Quality::ceilOutStrict()`. This adjustment maintains the offer's exchange rate while scaling down the size to match available funds, enabling offers to partially fill when the owner has insufficient balance. The adjustment rounds down under the `fixReducedOffersV1` amendment to prevent order book blocking where dust amounts could prevent offer consumption. @@ -1043,9 +1043,9 @@ BookStep handles three asset types, each with different authorization requiremen - **XRP**: No authorization checks required. Any account can hold XRP. - **Tokens (IOUs)**: Checked via `requireAuth`, which verifies the offer owner either has a trust line to the token issuer, or the issuer doesn't require authorization (`lsfRequireAuth` flag). If the issuer requires auth and the owner lacks the appropriate auth flag on their trust line, the offer is marked for removal. -- **MPTs**: Require both `requireAuth` (which checks the [`lsfMPTAuthorized`](../mpts/README.md#2221-flags) flag on the holder's MPToken) and [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed) (which verifies the [`lsfMPTCanTrade`](../mpts/README.md#2121-flags) flag and lock status). When crossing offers where the owner will receive an MPT, if the owner doesn't have an MPToken entry, BookStep automatically creates it via `checkCreateMPT`. +- **MPTs**: Require `requireAuth` (which checks the [`lsfMPTAuthorized`](../mpts/README.md#2221-flags) flag on the holder's MPToken) and `checkMPTDEX`, the MPT DEX permission check (it runs [`canTrade`](../mpts/README.md#361-cantrade) on both book assets and, where the owner is not the issuer, [`canTransfer`](../mpts/README.md#362-cantransfer) for the [`lsfMPTCanTransfer`](../mpts/README.md#2121-flags) flag). When crossing offers where the owner will receive an MPT, if the owner doesn't have an MPToken entry, BookStep automatically creates it via `checkCreateMPT`. -[^bookstep-auth]: Asset authorization checks and MPToken creation in BookStep: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L730-L764) +[^bookstep-auth]: Asset authorization checks and MPToken creation in BookStep: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L729-L760) **Pseudocode notes:** @@ -1055,7 +1055,7 @@ Throughout this section, member variables suffixed with `_` (e.g., `book_`, `own ## 5.1. `revImp` Implementation -[^bookstep-revimp]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1018-L1128) +[^bookstep-revimp]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L998-L1105) `BookStep::revImp`[^bookstep-revimp] executes the reverse pass, determining how much input is required to produce a desired output amount by consuming offers from the order book and AMM liquidity. Working backwards from the desired output, the method iterates through offers sorted by quality (best rates first), calculating how much of each offer can be consumed after accounting for transfer fees. It integrates AMM liquidity when available and competitive with CLOB offers, accumulating the total input required as it processes each liquidity source. The iteration continues until the desired output is satisfied or all available liquidity is exhausted, tracking which offers should be removed (expired, unfunded, or fully consumed) along the way. @@ -1147,7 +1147,7 @@ def revImp(sb: PaymentSandbox, afView: ApplyView, offersToRemove: &List[Offer], # Check if we consumed too many offers. Prevents DoS attacks where malicious actors # create many tiny unfunded offers to force expensive iteration - if offersConsumed >= maxOffersToConsume: + if offersConsumed >= kMaxOffersToConsume: # Use the liquidity we found but mark this path as "dry" so it won't be tried # again in future iterations of this payment inactive_ = True @@ -1170,7 +1170,7 @@ def revImp(sb: PaymentSandbox, afView: ApplyView, offersToRemove: &List[Offer], ## 5.2. `fwdImp` Implementation -[^bookstep-fwdimp]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L1132-L1292) +[^bookstep-fwdimp]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L1109-L1267) `BookStep::fwdImp`[^bookstep-fwdimp] executes the forward pass, consuming available input to produce output. Unlike `revImp`, it asserts that the cache is already set from the reverse pass. A key aspect is handling rounding differences: the forward pass may produce more output than the reverse pass while consuming the same input (or less). When this occurs, `fwdImp` computes the input required to produce the cached output (from the reverse pass). If that input equals the input consumed in the forward pass, it uses the cached output; otherwise it keeps the forward calculation result. @@ -1304,7 +1304,7 @@ def fwdImp(sb: PaymentSandbox, afView: ApplyView, offersToRemove: &List[Offer], offersToRemove.union(removedOffers) # Check if we consumed too many offers. Prevents DoS attacks - if offersConsumed >= MaxOffersToConsume: + if offersConsumed >= kMaxOffersToConsume: # Use the liquidity but mark strand as inactive (dry) inactive_ = True @@ -1327,7 +1327,7 @@ def fwdImp(sb: PaymentSandbox, afView: ApplyView, offersToRemove: &List[Offer], ## 5.3. `forEachOffer` -[^bookstep-foreachoffer]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L685-L874) +[^bookstep-foreachoffer]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L686-L853) `BookStep::forEachOffer`[^bookstep-foreachoffer] iterates through available liquidity sources (order book offers and AMM offers) in quality order, calling a provided callback for each valid offer until the payment requirements are satisfied. @@ -1338,7 +1338,7 @@ Given a callback function and the previous step's debt direction, this function: 4. For each offer, performs asset-specific validation: - Creates MPToken for the offer owner if input is MPT (if it does not already exist) - Validates authorization via `requireAuth` (Tokens check trust lines, MPTs check holder authorization) - - For MPTs, validates DEX trading permissions via `checkMPTDEXAllowed` + - For MPTs, validates DEX/transfer permission via `checkMPTDEX` (`canTrade` plus `canTransfer`) - Checks offer funding and calculates transfer fees - For MPT input on the first step, limits input amount to prevent issuer overflow. `MaximumAmount - OutstandingAmount` should never become negative. 5. Calls the callback with calculated amounts (offer amount, step amount, owner gives, transfer rates) @@ -1406,9 +1406,9 @@ def forEachOffer(sb: PaymentSandbox, afView: ApplyView, # Code comment: Make sure offer owner has authorization to own Assets from issuer # if IOU. An account can always own XRP or their own Assets. # If MPT then MPTDEX should be allowed. + # requireAuth uses applyView; the MPT DEX check (checkMPTDEX) uses sb. if (requireAuth(applyView, assetIn, owner) != tesSUCCESS or - (isAssetInMPT and checkMPTDEXAllowed(applyView, assetIn, owner, None) != tesSUCCESS) or - (isAssetOutMPT and checkMPTDEXAllowed(applyView, assetOut, owner, None) != tesSUCCESS)): + not checkMPTDEX(sb, owner)): # Code comment: Offer owner not authorized to hold IOU/MPT from issuer. # Remove this offer even if no crossing occurs. if offer.key(): @@ -1452,7 +1452,7 @@ def forEachOffer(sb: PaymentSandbox, afView: ApplyView, stpAmt.out = mulRatio(ownerGives, QUALITY_ONE, ofrOutRate, roundUp=False) # Code comment: It turns out we can prevent order book blocking by (strictly) - # rounding down the ceil_out() result. This adjustment changes + # rounding down the ceilOutStrict() result. This adjustment changes # transaction outcomes, so it must be made under an amendment. ofrAmt = offer.limitOut(ofrAmt, stpAmt.out, roundUp=False) @@ -1463,7 +1463,7 @@ def forEachOffer(sb: PaymentSandbox, afView: ApplyView, # by the issuer. Otherwise, OutstandingAmount may overflow. issuer = assetIn.getIssuer() if isAssetInMPT and not prevStep_ and owner != issuer: - available = accountHolds(sb, issuer, assetIn, fhIGNORE_FREEZE, ahIGNORE_AUTH) + available = accountFunds(sb, issuer, assetIn, FreezeHandling::IgnoreFreeze, AuthHandling::IgnoreAuth) if stpAmt.in > available: limitStepIn(offer, ofrAmt, stpAmt, ownerGives, ofrInRate, ofrOutRate, available) @@ -1505,7 +1505,7 @@ def forEachOffer(sb: PaymentSandbox, afView: ApplyView, The `getOfrInRate` and `getOfrOutRate` functions[^bookstep-transfer-rate-helpers] calculate transfer rates to be applied when consuming offers in `forEachOffer`. These functions exist in both `BookPaymentStep` and `BookOfferCrossingStep` but have different implementations: -[^bookstep-transfer-rate-helpers]: BookPaymentStep: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L290-L304), BookOfferCrossingStep: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L464-L490) +[^bookstep-transfer-rate-helpers]: BookPaymentStep: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L326-L336), BookOfferCrossingStep: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L486-L508) **BookPaymentStep**: Always returns the input rates unchanged. No transfer fee waiving occurs during payment execution - all fees are applied as specified. @@ -1574,11 +1574,11 @@ AMM offers have a **spot price quality**[^amm-spot-price-quality] determined by SpotPriceQuality = AssetIn / AssetOut ``` -[^amm-spot-price-quality]: [`Quality.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Quality.cpp#L16-L19) +[^amm-spot-price-quality]: [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Quality.cpp#L17-L19) Before generating an AMM offer, `AMMLiquidity::getOffer()`[^amm-spot-price-comparison] compares the AMM's spot price quality with the best available CLOB quality. Since the spot price represents the AMM's best possible quality before any swap, and any actual offer will have worse quality due to slippage, checking the spot price provides an early filter: if the spot price quality already cannot beat the CLOB, then any sized AMM offer will also fail to compete. -[^amm-spot-price-comparison]: [`AMMLiquidity.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMLiquidity.cpp#L164-L170) +[^amm-spot-price-comparison]: [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L184-L190) - If `SpotPriceQuality <= CLOBQuality` or the two qualities are within 1e-7 relative distance, the AMM returns `std::nullopt` (cannot compete) - Otherwise, an AMM offer is generated @@ -1587,7 +1587,7 @@ Before generating an AMM offer, `AMMLiquidity::getOffer()`[^amm-spot-price-compa Once `AMMLiquidity::getOffer()`[^amm-getoffer-sizing] determines the AMM can compete (spot price quality beats CLOB), it must decide how much liquidity to offer. The sizing strategy depends on whether the payment uses multiple paths or a single path. -[^amm-getoffer-sizing]: [`AMMLiquidity.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMLiquidity.cpp#L172-L206) +[^amm-getoffer-sizing]: [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L192-L222) The sizing strategy branches based on `ammContext_.multiPath()`: @@ -1605,7 +1605,7 @@ def getOffer(view: ReadView, clobQuality: Optional[Quality]) -> Optional[AMMOffe # ammContext is instance variable if ammContext.maxItersReached(): - return None # Already consumed AMM liquidity 30 times + return None # AMM liquidity already consumed in 30 iterations (kMaxIterations cap) # Fetch current pool balances from ledger # balances.in: Amount of input currency in the pool (what taker pays to pool) @@ -1635,12 +1635,12 @@ def getOffer(view: ReadView, clobQuality: Optional[Quality]) -> Optional[AMMOffe baseOut = swapAssetIn(initialBalances, offerIn, tradingFee) # ../amms/helpers.md#311-swapassetin # Scale by Fibonacci number for this iteration - fibonacci = [1, 1, 2, 3, 5, 8, 13, 21 ... 1346269] + fibonacci = [1, 1, 2, 3, 5, 8, 13, 21 ... 832040] fibMultiplier = fibonacci[iteration] offerOut = baseOut * fibMultiplier # Calculate required input to produce scaled output - offerIn = swapAssetOut(balances, offerOut, tradingFee) # ../amms/helpers.md#212-swapassetout + offerIn = swapAssetOut(balances, offerOut, tradingFee) # ../amms/helpers.md#312-swapassetout amounts = {in: offerIn, out: offerOut} if clobQuality and Quality(amounts) < clobQuality: @@ -1651,7 +1651,7 @@ def getOffer(view: ReadView, clobQuality: Optional[Quality]) -> Optional[AMMOffe if not amounts: # Return the biggest size this AMM can provide - maxAMMOffer = maxOffer(balances, tradingFee) + maxAMMOffer = maxOffer(balances, rules) # If maxAMMOffer provides better quality than clobQuality if maxAMMOffer and Quality(maxAMMOffer.amounts) > clobQuality: @@ -1661,7 +1661,7 @@ def getOffer(view: ReadView, clobQuality: Optional[Quality]) -> Optional[AMMOffe else: # No CLOB to compete against, offer maximum liquidity # With fixAMMOverflowOffer: 99% of pool balance - amounts = maxOffer(balances, tradingFee) + amounts = maxOffer(balances, rules) offerQuality = amounts.in / amounts.out return AMMOffer( @@ -1676,7 +1676,9 @@ def getOffer(view: ReadView, clobQuality: Optional[Quality]) -> Optional[AMMOffe Multi-path payments have multiple strands competing to provide liquidity. The Payment engine uses an iterative algorithm: in each iteration, it evaluates all available strands, ranks them by quality (exchange rate), and executes the highest-quality strand. This process repeats until the payment is fulfilled, ensuring the sender gets the best overall rate by consuming the most efficient liquidity sources first. `BookStep::forEachOffer()` alternates between CLOB and AMM offers, selecting whichever provides better quality. -When ammContext is `multiPath`, AMM offers are sized using a Fibonacci sequence. In the first iteration, as tracked in `ammContext`, AMM creates a synthetic offer that is 0.025% of the pool size. Each subsequent offer from a single AMM pool gets multiplied by the next number in the Fibonacci sequence. +When ammContext is `multiPath`, AMM offers are sized using a Fibonacci sequence. In the first iteration, as tracked in `ammContext`, AMM creates a synthetic offer that is 0.025% of the pool size.[^multipath-iter0] Each subsequent offer from a single AMM pool gets multiplied by the next number in the Fibonacci sequence. + +[^multipath-iter0]: The source returns this base (iteration-0) offer directly; the Fibonacci scaling and the `swapAssetOut` input recompute shown in the pseudo-code apply only from iteration 1 onward (`kFib` is indexed by `curIters() - 1`): [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L63-L100) As the desired offer output increases, the required input progressively increases, degrading the exchange rate for the taker. See [Swap Formulas](../amms/helpers.md#313-slippage-and-quality-degradation) for slippage examples. @@ -1688,14 +1690,14 @@ If there are no CLOB offers that can compete, AMM pool will quickly offer a larg **Algorithm**:[^amm-generate-fib-seq] -[^amm-generate-fib-seq]: [`AMMLiquidity.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMLiquidity.cpp#L40-L77) +[^amm-generate-fib-seq]: [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L65-L100) Each `AMMLiquidity` instance tracks the pool's initial balances (when the BookStep was created) and fetches current balances before each offer generation. This allows the algorithm to maintain consistent base sizing while accounting for liquidity changes as offers are consumed. 1. Calculate base offer: `0.00025 * initialBalances.in` (0.025% of the pool's input asset balance at BookStep creation) -2. Apply trading fee to base: `baseOut = `[`swapAssetIn`](../amms/helpers.md#211-swapassetin)`(initialBalances, baseOfferIn, fee)` - calculates output for the base-sized input using the initial pool state +2. Apply trading fee to base: `baseOut = `[`swapAssetIn`](../amms/helpers.md#311-swapassetin)`(initialBalances, baseOfferIn, fee)` - calculates output for the base-sized input using the initial pool state 3. Scale by Fibonacci: `scaledOut = baseOut * fib[iteration]` where `fib = [1, 1, 2, 3, 5, 8, 13, ...]` - grows the offer size exponentially across iterations -4. Recalculate input with current state: `scaledIn = `[`swapAssetOut`](../amms/helpers.md#212-swapassetout)`(currentBalances, scaledOut, fee)` - determines required input for the scaled output using current pool balances (which may have changed as previous offers were consumed) +4. Recalculate input with current state: `scaledIn = `[`swapAssetOut`](../amms/helpers.md#312-swapassetout)`(currentBalances, scaledOut, fee)` - determines required input for the scaled output using current pool balances (which may have changed as previous offers were consumed) #### 5.4.3.3. Single-Path Mode: CLOB-Matching Sizing @@ -1703,7 +1705,7 @@ Single-path payments have no alternative liquidity sources to balance. The AMM s **Case 1: No CLOB Quality Available**[^amm-no-clob] -[^amm-no-clob]: [`AMMLiquidity.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMLiquidity.cpp#L183-L190) +[^amm-no-clob]: [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L202-L209) If there is no CLOB offer to compete against (`clobQuality == std::nullopt`): @@ -1713,7 +1715,7 @@ If there is no CLOB offer to compete against (`clobQuality == std::nullopt`): **Case 2: CLOB Quality Available**[^amm-change-spot-price] -[^amm-change-spot-price]: [`AMMLiquidity.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/AMMLiquidity.cpp#L192-L198) +[^amm-change-spot-price]: [`AMMLiquidity.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/AMMLiquidity.cpp#L210-L215) The [`changeSpotPriceQuality()`](../amms/helpers.md#314-changespotpricequality) function calculates an offer size such that after the swap, the AMM's new spot price quality equals the CLOB quality: @@ -1733,7 +1735,7 @@ The `maxOffer()` function provides the largest possible offer from the pool: The `qualityUpperBound` method[^bookstep-qualityupperbound] estimates the best quality the BookStep can achieve. It retrieves the best offer quality (from either CLOB or AMM) and adjusts it for transfer fees. -[^bookstep-qualityupperbound]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L552-L574) +[^bookstep-qualityupperbound]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L572-L586) This quality estimate is used for [strand sorting](README.md#6-iterative-strands-evaluation-strandsflow) during multi-path payment evaluation and, when a `limitQuality` constraint is specified, for calculating the maximum output amount that maintains the required quality threshold in single-path payments. @@ -1746,7 +1748,7 @@ For offer crossing, the adjustment strategy differs: CLOB offers and multi-path The `qualityUpperBound` method[^bookstep-qualityupperbound-impl] for BookStep retrieves the best available offer quality from the order book (either the top CLOB offer or AMM spot price), determines the debt direction for this step, and adjusts the quality to account for transfer fees. For AMM offers, transfer fees are waived on output. -[^bookstep-qualityupperbound-impl]: [`BookStep.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/BookStep.cpp#L552-L574) +[^bookstep-qualityupperbound-impl]: [`BookStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L572-L586) The method calls the polymorphic `adjustQualityWithFees()` function, which has different implementations for [payment steps](#554-adjustqualitywithfees---bookpaymentstep-implementation) versus [offer crossing steps](#555-adjustqualitywithfees---bookoffercrossingstep-implementation). @@ -1780,17 +1782,17 @@ The `tipOfferQuality` method returns the best quality available at the tip of th For CLOB offers, the quality is retrieved using the BookTip iterator class[^booktip-class]. BookTip traverses offers in an order book from the highest quality to lowest quality by navigating the directory structure where qualities are encoded in the 8 rightmost bytes of directory index keys. The BookTip constructor[^booktip-constructor] takes a `Book` parameter, which contains the asset pair (in/out currencies and issuers) and an optional `domain` field. When a domain is specified, BookTip looks up the domain-specific order book directory[^booktip-domain] computed as `hash(BOOK_NAMESPACE, asset_in, asset_out, domainID)`, ensuring only domain offers are traversed. The `step()` method[^booktip-step] searches the directory for the first offer page, extracts the quality from the index[^booktip-extract-quality], and retrieves the corresponding offer ledger entry. The `quality()` method[^booktip-quality-method] then returns this extracted quality value. -[^booktip-class]: [`BookTip.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/BookTip.h#L16-L62) +[^booktip-class]: [`BookTip.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/BookTip.h#L15-L61) -[^booktip-constructor]: [`BookTip.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/BookTip.cpp#L5-L11) +[^booktip-constructor]: [`BookTip.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookTip.cpp#L15-L18) -[^booktip-domain]: [`Indexes.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Indexes.cpp#L102-L104) +[^booktip-domain]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Indexes.cpp#L107-L109) -[^booktip-step]: [`BookTip.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/BookTip.cpp#L13-L60) +[^booktip-step]: [`BookTip.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookTip.cpp#L20-L67) -[^booktip-extract-quality]: [`BookTip.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/BookTip.cpp#L42) +[^booktip-extract-quality]: [`BookTip.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookTip.cpp#L49) -[^booktip-quality-method]: [`BookTip.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/BookTip.h#L44-L48) +[^booktip-quality-method]: [`BookTip.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/paths/BookTip.h#L43-L47) For AMM offers, a synthetic offer is generated based on the current pool state (see [section 5.4.3](#543-offer-generation-strategies) for the complete algorithm). The AMM offer generation takes the CLOB quality as a threshold parameter - if the AMM's spot price quality cannot beat this threshold, no AMM offer is generated. @@ -1805,10 +1807,15 @@ For payments, apply transfer fees conditionally based on debt direction and offe ```python def adjustQualityWithFees(ofrQ, prevStepDir, waiveFee, offerType) -> Quality: def rate(asset): - if isXRP(asset) or asset.getIssuer() == strandDst_: + if isXRP(asset): return QUALITY_ONE - if asset.isToken(): + if asset.isToken(): # IOU + if asset.getIssuer() == strandDst_: + return QUALITY_ONE return transferRate(asset.getIssuer()) + # MPT: parity only when this is the strand's delivered asset AND its issuer is the destination + if asset == strandDeliver_ and asset.getIssuer() == strandDst_: + return QUALITY_ONE return transferRate(asset.getMptID()) # Input transfer rate: only charged when previous step redeems @@ -1821,7 +1828,7 @@ def adjustQualityWithFees(ofrQ, prevStepDir, waiveFee, offerType) -> Quality: trOut = QUALITY_ONE # Compose transfer fees with offer quality - feeQuality = Quality(trOut / trIn) + feeQuality = Quality(getRate(trOut, trIn)) return composedQuality(feeQuality, ofrQ) ``` @@ -1854,16 +1861,21 @@ def adjustQualityWithFees(ofrQ, prevStepDir, waiveFee, offerType) -> Quality: # Single-path AMM offers: must factor in transfer-in rate # Single-path AMM offer quality is not constant (changes as pool depletes) def rate(asset): - if isXRP(asset) or asset.getIssuer() == strandDst_: + if isXRP(asset): return QUALITY_ONE - if asset.isToken(): + if asset.isToken(): # IOU + if asset.getIssuer() == strandDst_: + return QUALITY_ONE return transferRate(asset.getIssuer()) + # MPT: parity only when this is the strand's delivered asset AND its issuer is the destination + if asset == strandDeliver_ and asset.getIssuer() == strandDst_: + return QUALITY_ONE return transferRate(asset.getMptID()) trIn = rate(book_.in) if redeems(prevStepDir) else QUALITY_ONE trOut = QUALITY_ONE # AMM doesn't pay transfer fee on output - feeQuality = Quality(trOut / trIn) + feeQuality = Quality(getRate(trOut, trIn)) return composedQuality(feeQuality, ofrQ) ``` @@ -1876,6 +1888,7 @@ The `check` method validates the BookStep configuration and ensures no loops exi - `temBAD_PATH`: Currency is inconsistent with issuer (XRP with non-XRP issuer or vice versa) - `temBAD_PATH_LOOP`: Book output asset already seen in another BookStep or DirectStep (prevents offer unfunding loops) - `tecNO_ISSUER`: Input or output issuer account does not exist +- For MPT assets (checked unconditionally, regardless of the previous step): `tecOBJECT_NOT_FOUND` if an MPT issuance does not exist, and a DEX permission check via [`canTrade`](../mpts/README.md#361-cantrade) on both the book input and output assets **Asset-specific checks (when previous step is DirectStep or MPTEndpointStep):** @@ -1883,6 +1896,4 @@ For **Tokens**: - `terNO_LINE`: Trust line does not exist between previous step source and book input issuer - `terNO_RIPPLE`: Trust line has NoRipple flag set -For **MPTs**: -- `tecOBJECT_NOT_FOUND`: MPT issuance does not exist -- DEX permission check via [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed) for the book input issuer \ No newline at end of file +For **MPTs**: the previous-step-gated block performs no additional check (it returns no error for MPT). \ No newline at end of file diff --git a/docs/mpts/README.md b/docs/mpts/README.md index 403c7c2..1120a6d 100644 --- a/docs/mpts/README.md +++ b/docs/mpts/README.md @@ -34,8 +34,9 @@ - [3.5.1. Failure Conditions](#351-failure-conditions) - [3.5.2. State Changes](#352-state-changes) - [3.6. MPT Validation Functions](#36-mpt-validation-functions) - - [3.6.1. checkMPTDEXAllowed](#361-checkmptdexallowed) - - [3.6.2. checkMPTTxAllowed](#362-checkmptxallowed) + - [3.6.1. canTrade](#361-cantrade) + - [3.6.2. canTransfer](#362-cantransfer) + - [3.6.3. canMPTTradeAndTransfer](#363-canmpttradeandtransfer) - [4. MPT Payment Execution](#4-mpt-payment-execution) - [4.1. Transfer Scenarios](#41-transfer-scenarios) - [4.1.1. Issuer Minting (Issuer -> Holder)](#411-issuer-minting-issuer---holder) @@ -44,24 +45,24 @@ # 1. Introduction -Multi-Purpose Tokens (MPTs) are a native asset type on the XRP Ledger designed to provide an alternative to traditional trust line-based tokens. An MPT consists of an `MPTokenIssuance` that defines the MPT's properties and configuration, and individual `MPToken` entries that track each holder's balance and holder-specific settings. +Multi-Purpose Tokens (MPTs) are a native asset type on the XRP Ledger designed to provide an alternative to traditional trust line-based tokens. An MPT consists of an `MPTokenIssuance` that defines the MPT's properties and configuration, and individual `MPToken` entries that track each holder's balance and holder-specific settings. The issuer does not hold an `MPToken` for its own issuance. The total amount in circulation is tracked on the `MPTokenIssuance` via `OutstandingAmount`, which is adjusted directly when the issuer mints to or burns from holders. -The lifecycle of an MPT begins when an issuer creates an MPT issuance using the [`MPTokenIssuanceCreate` transaction](#31-mptokenissuancecreate-transaction). This transaction defines the MPT's capabilities through capability flags (such as `lsfMPTRequireAuth`, `lsfMPTCanTransfer`, and `lsfMPTCanClawback`) and optional properties like `TransferFee` and `MaximumAmount`. Capability flags are immutable by default, but the issuer can grant mutability permissions at creation time to allow changing them later via `MPTokenIssuanceSet`. See [MPTokenIssuance Fields](#212-fields) for complete details on mutable and immutable properties. +An MPT moves through the following lifecycle: -Once an issuance exists, holders acquire the ability to hold the MPT by sending an [`MPTokenAuthorize` transaction](#34-mptokenauthorize-transaction), which creates their `MPToken` entry. If the issuance has the `lsfMPTRequireAuth` flag set (on the `MPTokenIssuance`), the holder's `MPToken` entry is created but not yet authorized. The issuer must then send their own `MPTokenAuthorize` transaction specifying the holder's account to set the `lsfMPTAuthorized` flag (on the holder's `MPToken`), allowing the holder to receive MPTs. If `lsfMPTRequireAuth` is not set, holders can immediately receive MPTs after creating their `MPToken` entry. See [MPToken Fields](#222-fields) for details on holder-specific settings and [Reserves](#224-reserves) for information about reserve requirements. +1. **Create the issuance.** The issuer sends an [`MPTokenIssuanceCreate` transaction](#31-mptokenissuancecreate-transaction), which defines the MPT's capability flags (such as `lsfMPTRequireAuth`, `lsfMPTCanTransfer`, and `lsfMPTCanClawback`) and optional properties like `TransferFee` and `MaximumAmount`. Capability flags are immutable by default, but the issuer can grant mutability permissions at creation time to allow changing them later via `MPTokenIssuanceSet`. See [MPTokenIssuance Fields](#212-fields). +2. **Authorize holders.** A holder sends an [`MPTokenAuthorize` transaction](#34-mptokenauthorize-transaction) to create their `MPToken` entry. If the issuance has `lsfMPTRequireAuth` set, the entry is created but not yet authorized. The issuer must then send their own `MPTokenAuthorize` naming the holder to set `lsfMPTAuthorized` (on the holder's `MPToken`) before the holder can receive MPTs. If `lsfMPTRequireAuth` is not set, the holder can receive MPTs as soon as the entry exists. See [MPToken Fields](#222-fields) and [Reserves](#224-reserves). +3. **Transfer.** Once authorized (if required), MPTs are transferred between holders via the standard `Payment` transaction, applying the issuer's `TransferFee` if configured. See [MPT Payment Execution](#4-mpt-payment-execution). -After authorization (if required), MPTs can be transferred between holders via the standard `Payment` transaction. During transfers, the issuer's `TransferFee` is applied if configured. See [MPT Payment Execution](#4-mpt-payment-execution) for details on how transfer fees are calculated and applied during payments. - -The issuer maintains control over the MPT through the [`MPTokenIssuanceSet` transaction](#33-mptokenissuanceset-transaction), which can toggle global locking (the `lsfMPTLocked` flag on the `MPTokenIssuance`) to freeze all operations, or toggle individual holder locking (the `lsfMPTLocked` flag on a holder's `MPToken`, requires the `lsfMPTCanLock` capability flag) to prevent a specific holder from sending or receiving MPTs. See [Flags](#2121-flags) for all lock-related capability flags and [Payment validation rules](#4-mpt-payment-execution) for how locks affect payment processing. +Throughout the MPT's life, the issuer retains control through the [`MPTokenIssuanceSet` transaction](#33-mptokenissuanceset-transaction): global locking (the `lsfMPTLocked` flag on the `MPTokenIssuance`) prevents all holders from transferring the MPT, and individual holder locking (the `lsfMPTLocked` flag on a holder's `MPToken`) prevents a single holder from transferring it. Both require the issuance to have the `lsfMPTCanLock` capability flag; neither prevents the issuer from minting to or redeeming from holders (issuer <-> holder transfers remain available even when locked). See [Flags](#2121-flags) and [Payment validation rules](#4-mpt-payment-execution). ## 1.1. DomainID and Authorization The `MPTokenIssuance` ledger entry has an optional `DomainID` field that controls **MPT authorization** - determining who can hold and receive the MPT. When `DomainID` is set on an `MPTokenIssuance`: - Accounts must have valid, non-expired credentials for the specified PermissionedDomain to be authorized as MPT holders -- Authorization is verified during Payment transactions when the MPT is being received +- Authorization is verified whenever an account would receive or acquire the MPT. This includes `Payment`, offer creation and crossing, AMM operations, escrow, and cross-currency flow steps - Requires both `lsfMPTRequireAuth` flag (see [Flags](#2121-flags)) and the `featurePermissionedDomains` and `featureSingleAssetVault` amendments -This MPT authorization mechanism is **separate and independent** from PermissionedDEX domain restrictions on offers. The `MPTokenIssuance.DomainID` controls who can hold and receive the MPT (verified during Payment transactions), while `Offer.DomainID` controls which order book an offer is placed in and restricts liquidity consumption to that domain's order book during payments and offer crossing (see [Domain Payments and AMM Exclusion](../flow/README.md#33-domain-payments)). MPTs can be traded in [domain offers](../offers/README.md#151-domain-offers), [open offers](../offers/README.md#15-permissioned-dex), and [hybrid offers](../offers/README.md#152-hybrid-offers) regardless of whether the `MPTokenIssuance` has a `DomainID` set - creating an offer with `Offer.DomainID` requires the offer creator to have domain access, but does not require the MPT itself to have a `DomainID`. +This MPT authorization mechanism is **separate and independent** from PermissionedDEX domain restrictions on offers. The `MPTokenIssuance.DomainID` controls who can hold and receive the MPT (verified whenever an account receives the MPT), while `Offer.DomainID` controls which order book an offer is placed in and restricts liquidity consumption to that domain's order book during payments and offer crossing (see [Domain Payments](../flow/README.md#33-domain-payments)). MPTs can be traded in [domain offers](../offers/README.md#151-domain-offers), [open offers](../offers/README.md#15-permissioned-dex), and [hybrid offers](../offers/README.md#152-hybrid-offers) regardless of whether the `MPTokenIssuance` has a `DomainID` set - creating an offer with `Offer.DomainID` requires the offer creator to have domain access, but does not require the MPT itself to have a `DomainID`. # 2. Ledger Entries @@ -80,6 +81,7 @@ classDiagram +uint64 LockedAmount +Blob MPTokenMetadata +uint32 Flags + +uint32 MutableFlags +uint256 DomainID } @@ -146,16 +148,19 @@ Where `sequence` is the issuer's sequence number at creation time and `issuer` i | `Sequence` | UInt32 | Yes | The issuer's sequence number at creation (used in MPTID calculation) | | `TransferFee` | UInt16 | Default | Transfer fee in 1/10 of basis points (0-50000, representing 0%-50%) | | `OwnerNode` | UInt64 | Yes | Index of the owner directory page for this issuance | -| `AssetScale` | UInt8 | Default | Number of decimal places for display (0-19). Display hint only; does not affect on-ledger integer arithmetic. | +| `AssetScale` | UInt8 | Default | Number of decimal places for display. Display hint only; does not affect on-ledger integer arithmetic. | | `MaximumAmount` | UInt64 | Optional | Maximum supply cap (must be > 0, cannot exceed 0x7FFFFFFFFFFFFFFF) | -| `OutstandingAmount` | UInt64 | Yes | Current total amount held across all holders | +| `OutstandingAmount` | UInt64 | Yes | Total amount in circulation. Equals the sum of every holder's balance, including escrow-locked amounts.[^outstanding-balance] | | `LockedAmount` | UInt64 | Optional | Amount currently locked in escrows | | `MPTokenMetadata` | Blob | Optional | Arbitrary metadata (1-1024 bytes) | | `MutableFlags` | UInt32 | Default | Mutability permissions for capability flags (see [Flags](#2121-flags)) | | `DomainID` | UInt256 | Optional | Permissioned domain identifier for MPT authorization (see [DomainID and Authorization](#11-domainid-and-authorization)) | +| `ReferenceHolding` | UInt256 | Optional | Vault-share issuances only. Points to the vault pseudo-account's holding of the underlying asset (an `MPToken` or `RippleState`). Set internally by `VaultCreate`. `canTrade` and `canTransfer` follow it so the share inherits the underlying asset's tradability and transferability. | | `PreviousTxnID` | UInt256 | Yes | Transaction hash that most recently modified this entry | | `PreviousTxnLgrSeq` | UInt32 | Yes | Ledger sequence of the transaction that most recently modified this entry | +[^outstanding-balance]: [`MPTInvariant.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/invariants/MPTInvariant.cpp#L398-L418), [`finalize`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/invariants/MPTInvariant.cpp#L454-L470) + **Field constraints**: - `TransferFee`: If non-zero, requires `lsfMPTCanTransfer` flag - `MaximumAmount`: If set, `OutstandingAmount` cannot exceed it @@ -166,8 +171,8 @@ Where `sequence` is the issuer's sequence number at creation time and `issuer` i | Flag Name | Hex Value | Description | |---------------------|--------------|----------------------------------------------------------| -| `lsfMPTLocked` | `0x00000001` | Global lock (freezes all MPT operations for all holders) | -| `lsfMPTCanLock` | `0x00000002` | Issuer can lock individual `MPToken` entries | +| `lsfMPTLocked` | `0x00000001` | Global lock. Prevents all holders from transferring the MPT. Direct issuer/holder transfers still allowed. | +| `lsfMPTCanLock` | `0x00000002` | Permits the issuer to lock the MPT, globally or per holder, via `MPTokenIssuanceSet`. | | `lsfMPTRequireAuth` | `0x00000004` | Holders must be authorized by issuer before transacting | | `lsfMPTCanEscrow` | `0x00000008` | MPT can be held in escrow | | `lsfMPTCanTrade` | `0x00000010` | MPT can be traded on the decentralized exchange | @@ -184,16 +189,18 @@ Where `sequence` is the issuer's sequence number at creation time and `issuer` i These flags control different, independent aspects of MPT movement: -- **`lsfMPTCanTrade`**: Required for all DEX operations. When set, the MPT can be listed in offers via [OfferCreate](../offers/README.md) (MPT/XRP, MPT/IOU, MPT/MPT pairs), deposited into [AMM pools](../amms/README.md), and used in [cross-currency payments](../payments/README.md#423-mpt-integration-in-cross-currency-payments) through order books and AMMs. Without this flag, all DEX operations fail with `tecNO_PERMISSION`, regardless of whether `lsfMPTCanTransfer` is set. DEX operations validate this flag through `checkMPTDEXAllowed()`, which also checks lock status and holder authorization. See [Section 3.6](#36-mpt-trading-on-dex) for complete DEX integration details. +- **`lsfMPTCanTrade`**: Required for all DEX operations. When set, the MPT can be listed in offers via [OfferCreate](../offers/README.md) (MPT/XRP, MPT/IOU, MPT/MPT pairs), deposited into [AMM pools](../amms/README.md), and used in [cross-currency payments](../payments/README.md#423-mpt-integration-in-cross-currency-payments) through order books and AMMs. Without this flag, all DEX operations fail with `tecNO_PERMISSION`, regardless of whether `lsfMPTCanTransfer` is set. DEX operations validate this flag through `canTrade()`; lock status and holder authorization are checked separately by `isFrozen` and `requireAuth`. See [Section 3.6](#36-mpt-validation-functions) for complete validation details. - **`lsfMPTCanTransfer`**: Required for direct holder-to-holder transfers without DEX involvement. When set, holders can send MPTs directly to other holders via Payment transactions using direct balance update logic (no pathfinding, no order books). This flag is only required when neither the sender nor the receiver is the issuer. Transfers involving the issuer (minting from issuer to holder, or burning from holder to issuer) do not require this flag. See [MPT Payment Execution](#4-mpt-payment-execution) for transfer mechanics. -| `lsfMPTCanTrade` | `lsfMPTCanTransfer` | Allowed Operations | -|------------------|---------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| ✅ Set | ✅ Set | All operations: DEX operations (offers, AMMs, cross-currency payments) AND direct holder-to-holder transfers | -| ✅ Set | ❌ Not set | DEX operations only when destination is the issuer (burning); issuer minting to holders; no holder-to-holder transfers or DEX operations that deliver to non-issuer holders | -| ❌ Not set | ✅ Set | Direct holder-to-holder transfers only; issuer-involved transfers (minting/burning); all DEX operations fail | -| ❌ Not set | ❌ Not set | Only issuer-involved transfers (minting/burning); no holder-to-holder transfers, no DEX operations | +| `lsfMPTCanTrade` | `lsfMPTCanTransfer` | Holder-to-holder transfers | DEX operations (offers, AMMs, cross-currency) | +|------------------|---------------------|----------------------------|-----------------------------------------------| +| ✅ Set | ✅ Set | ✅ Allowed | ✅ Allowed | +| ✅ Set | ❌ Not set | ❌ Blocked (`tecNO_AUTH`) | ⚠️ Only legs delivering to the issuer (e.g. burning); delivery to a non-issuer holder fails with `tecNO_AUTH` | +| ❌ Not set | ✅ Set | ✅ Allowed | ❌ Blocked (`tecNO_PERMISSION`) | +| ❌ Not set | ❌ Not set | ❌ Blocked (`tecNO_AUTH`) | ❌ Blocked (`tecNO_PERMISSION`) | + +Transfers involving the issuer (minting and burning) are always allowed and require neither flag. A DEX trade runs both checks: `canTrade` gates the trade, and `canTransfer` gates the delivery leg to a non-issuer holder. ### 2.1.3. Pseudo-accounts @@ -216,7 +223,9 @@ Creating an `MPTokenIssuance` always requires one owner reserve. The issuer's `O ## 2.2. MPToken Ledger Entry -The `MPToken` ledger entry (type `ltMPTOKEN = 0x007f`) tracks an individual holder's balance and settings for a specific MPT issuance. +The `MPToken` ledger entry (type `ltMPTOKEN = 0x007f`)[^mpt-ledger-layout] tracks an individual holder's balance and settings for a specific MPT issuance. + +[^mpt-ledger-layout]: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/ledger_entries.macro#L409-L417) ### 2.2.1. Object Identifier @@ -228,7 +237,9 @@ concatenated in order: - The `MPTokenIssuance` key - The holder's `AccountID` -The `MPTokenIssuance` key is calculated as `SHA512-Half(0x007E, MPTID)` where `0x007E` is the `MPTokenIssuance` space key and `MPTID` is the 192-bit issuance identifier. +The `MPTokenIssuance` key is calculated as `SHA512-Half(0x007E, MPTID)` where `0x007E` is the `MPTokenIssuance` space key and `MPTID` is the 192-bit issuance identifier.[^mpt-keylet] + +[^mpt-keylet]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Indexes.cpp#L533-L541) ### 2.2.2. Fields @@ -236,15 +247,18 @@ The `MPTokenIssuance` key is calculated as `SHA512-Half(0x007E, MPTID)` where `0 |---------------------|-----------|----------|---------------------------------------------------------------------------| | `Account` | AccountID | Yes | The holder's account | | `MPTokenIssuanceID` | UInt192 | Yes | Reference to the MPT issuance (MPTID) | -| `MPTAmount` | UInt64 | Default | Amount held by this account (max 0x7FFFFFFFFFFFFFFF) | -| `LockedAmount` | UInt64 | Optional | Amount locked in escrows | +| `MPTAmount` | UInt64 | Default | Available (spendable) balance held by this account (max `0x7FFFFFFFFFFFFFFF`) | +| `LockedAmount` | UInt64 | Optional | Portion of the holder's balance locked in MPT escrows, held separately from `MPTAmount` | | `OwnerNode` | UInt64 | Yes | Index of the owner directory page for this holder | | `PreviousTxnID` | UInt256 | Yes | Transaction hash that most recently modified this entry | | `PreviousTxnLgrSeq` | UInt32 | Yes | Ledger sequence of the transaction that most recently modified this entry | **Field constraints**: -- `MPTAmount`: Must not exceed `MaximumAmount` on the issuance (if set) -- `LockedAmount`: If present, requires `lsfMPTCanEscrow` on the issuance +- `MPTAmount`: bounded by `kMaxMpTokenAmount` (2^63-1, `0x7FFFFFFFFFFFFFFF`). The issuance-wide `MaximumAmount` (if set) caps the issuance's total `OutstandingAmount` across all holders, not any single holder's balance.[^mpt-supply-cap] +- `LockedAmount`: only ever populated by MPT escrows. Escrow creation requires `lsfMPTCanEscrow` on the issuance, so a non-zero `LockedAmount` implies the issuance has `lsfMPTCanEscrow` set.[^mpt-locked-escrow] + +[^mpt-supply-cap]: [`isMPTOverflow`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L973-L983), [`maxMPTAmount`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L949-L960) +[^mpt-locked-escrow]: [`EscrowCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/escrow/EscrowCreate.cpp#L279-L281) #### 2.2.2.1. Flags @@ -258,11 +272,13 @@ The `lsfMPTLocked` flag can only be set if the issuance has `lsfMPTCanLock` flag The `lsfMPTAuthorized` flag is only relevant when the issuance has `lsfMPTRequireAuth` flag set. -The `lsfMPTAMM` flag is automatically set when an AMM creates an MPToken entry for an MPT asset in its pool. See [AMM documentation](../amms/README.md) for details on AMM pseudo-account behavior. +The `lsfMPTAMM` flag is automatically set when an AMM creates an `MPToken` for an MPT in its pool (which requires the MPTokensV2 amendment). The AMM's `MPToken` is created with both `lsfMPTAMM` and `lsfMPTAuthorized`, even when the issuance has `lsfMPTRequireAuth`. AMM pseudo-accounts, like Vault and LoanBroker pseudo-accounts, are implicitly authorized, so `requireAuth` succeeds for them and they can hold a require-auth MPT. This implicit authorization cannot be revoked. An `MPTokenAuthorize` transaction naming an AMM, Vault, or LoanBroker pseudo-account as `Holder` fails with `tecNO_PERMISSION`. See [AMM documentation](../amms/README.md) for AMM pseudo-account behavior.[^mpt-amm-auth] + +[^mpt-amm-auth]: [`AMMCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/AMMCreate.cpp#L310-L319), [`requireAuth`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L385-L390), [`MPTokenAuthorize.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp#L134-L139) ### 2.2.3. Pseudo-accounts -MPToken entries do not require pseudo-accounts. +MPToken entries do not require pseudo-accounts. The holder is a regular ledger account. ### 2.2.4. Ownership @@ -275,7 +291,9 @@ MPToken reserves are determined by the account's total `OwnerCount`: - **OwnerCount < 2**: No reserve required for creating `MPToken` - **OwnerCount >= 2**: One owner reserve per `MPToken` -The holder's `OwnerCount` is always incremented when an `MPToken` is created and decremented when deleted. This differs from trust lines, which only increment `OwnerCount` when in non-default state. +The holder's `OwnerCount` is always incremented when an `MPToken` is created and decremented when deleted. This differs from trust lines, which only increment `OwnerCount` when in non-default state. The reserve grace for the first two owned items, however, mirrors trust-line reserve behavior: no incremental reserve is enforced while `OwnerCount` is below 2.[^mpt-reserve] + +[^mpt-reserve]: [`MPTokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L193-L204) # 3. Transactions @@ -287,14 +305,16 @@ The `MPTokenIssuanceCreate` transaction creates a new MPT issuance with specifie |-------------------|:------------------:|:-----------:|:----------------------:|:-------------:|:-------------:|:-------------------------------------------------------------------------------------------------------------------------------------| | `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"MPTokenIssuanceCreate"` | | `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account creating the issuance (becomes the issuer) | -| `AssetScale` | | `No` | `Number` | `UInt8` | `0` | Number of decimal places for display (0-19). Specifies how many decimal places the MPT can be subdivided. | -| `TransferFee` | | `No` | `Number` | `UInt16` | `0` | Transfer fee in tenths of a basis point (0-50000 inclusive, representing 0%-50%). Must not be present if `tfMPTCanTransfer` is not set. | +| `AssetScale` | | `No` | `Number` | `UInt8` | `0` | Number of decimal places for display. Any `UInt8` value (0-255) is accepted. | +| `TransferFee` | | `Conditional` | `Number` | `UInt16` | `0` | Transfer fee in tenths of a basis point (0-50000 inclusive, representing 0%-50%). Must not be present if `tfMPTCanTransfer` is not set. | | `MaximumAmount` | | `No` | `String - Number` | `UInt64` | | Maximum supply cap. Valid range: 1 to 2^63-1. | -| `MPTokenMetadata` | | `No` | `String - Hexadecimal` | `Blob` | | Arbitrary metadata (1-1024 bytes). By convention, should decode to JSON describing what the MPT represents. | +| `MPTokenMetadata` | | `Conditional` | `String - Hexadecimal` | `Blob` | | Arbitrary metadata (1-1024 bytes). By convention, should decode to JSON describing what the MPT represents. | | `DomainID` | | `No` | `String - Hexadecimal` | `UInt256` | | Permissioned domain identifier (requires amendments) | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Capability flags | +| `Flags` | | `Conditional` | `Number` | `UInt32` | `0` | Capability flags | | `MutableFlags` | | `No` | `Number` | `UInt32` | `0` | Mutability flags. Requires [DynamicMPT](https://github.com/XRPLF/XRPL-Standards/tree/master/XLS-0094-dynamic-MPT) amendment. | +`Conditional` modifiability (in the table above) means the field or its capability flags can be changed after creation via `MPTokenIssuanceSet`, but only if the matching `tmfMPTCanMutate*` flag was set at creation (requires the DynamicMPT amendment). + **Transaction Flags (Capability Flags)**: | Flag Name | Hex Value | Description | @@ -306,6 +326,8 @@ The `MPTokenIssuanceCreate` transaction creates a new MPT issuance with specifie | `tfMPTCanTransfer` | `0x00000020` | Enable transfers between accounts | | `tfMPTCanClawback` | `0x00000040` | Enable issuer clawback | +There is no flag to lock the issuance at creation. `lsfMPTLocked` is intentionally not settable here; an issuance is always created unlocked and can be locked later via `MPTokenIssuanceSet`. + **MutableFlags (Mutability Flags)**: These flags control whether the corresponding capability flags can be changed after creation via `MPTokenIssuanceSet`: @@ -325,13 +347,15 @@ These flags control whether the corresponding capability flags can be changed af **Static validation**[^mptissuancecreate-static-validation] -[^mptissuancecreate-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp#L10-L22), [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp#L25-L29), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp#L32-L78) +[^mptissuancecreate-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp#L30-L41), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp#L44-L48), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp#L51-L101) - `temDISABLED`: - [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) amendment is not enabled - `DomainID` is specified but amendments not enabled (requires both [PermissionedDomains](https://xrpl.org/resources/known-amendments#permissioneddomains) and [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault)) - `MutableFlags` is specified but [DynamicMPT](https://xrpl.org/resources/known-amendments#dynamicmpt) amendment is not enabled -- `temINVALID_FLAG`: Invalid flags or `MutableFlags` specified +- `temINVALID_FLAG`: + - `Flags` contains a bit outside the allowed capability-flag set + - `MutableFlags` is present but is zero, or contains a bit outside the allowed mutability-flag set (when `MutableFlags` is included, at least one valid mutability bit must be set) - `temBAD_TRANSFER_FEE`: `TransferFee` exceeds 50000 (50%) - `temMALFORMED`: - `TransferFee` is non-zero but `tfMPTCanTransfer` is not set @@ -339,12 +363,13 @@ These flags control whether the corresponding capability flags can be changed af - `DomainID` is specified but `tfMPTRequireAuth` is not set - `MPTokenMetadata` length is not between 1 and 1024 bytes - `MaximumAmount` is zero or exceeds `0x7FFFFFFFFFFFFFFF` + - `ReferenceHolding` is present (it is set internally by `VaultCreate` only and cannot be supplied by user transactions; gated on the `fixCleanup3_2_0` amendment) **Validation during doApply**[^mptissuancecreate-doapply-validation] -[^mptissuancecreate-doapply-validation]: Validation during doApply: [`MPTokenIssuanceCreate.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceCreate.cpp#L143-L162) +[^mptissuancecreate-doapply-validation]: Validation during doApply (reserve, directory, and internal checks in `create`): [`MPTokenIssuanceCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceCreate.cpp#L104-L172) -- `tecINSUFFICIENT_RESERVE`: Account has insufficient XRP balance to cover the reserve for creating the issuance (one owner reserve required) +- `tecINSUFFICIENT_RESERVE`: the account's pre-fee balance is below the reserve required for one additional owned object (base reserve plus per-owner increments) - `tecDIR_FULL`: Owner directory is full and cannot accommodate the new issuance - `tecINTERNAL`: Signing account does not exist @@ -352,13 +377,13 @@ These flags control whether the corresponding capability flags can be changed af - `MPTokenIssuance` object is **created**: - `Issuer`: Set to signing account - - `Sequence`: Set to account's current sequence (before increment) + - `Sequence`: Set to the transaction's sequence value (the account `Sequence`, or the `TicketSequence` if submitted with a Ticket). This same value feeds the MPTID derivation. - `OutstandingAmount`: Set to 0 - `OwnerNode`: Set to directory page index - `Flags`: Set to transaction flags (excluding universal flags) - - `MutableFlags`: Set if provided (default 0) - - `AssetScale`: Set if provided (default 0) - - `TransferFee`: Set if provided (default 0) + - `MutableFlags`: Set if provided + - `AssetScale`: Set if provided + - `TransferFee`: Set if provided - `MaximumAmount`: Set if provided - `MPTokenMetadata`: Set if provided - `DomainID`: Set if provided @@ -379,20 +404,20 @@ The `MPTokenIssuanceDestroy` transaction deletes an MPT issuance. This can only | `TransactionType` | :heavy_check_mark: | `No` | `String` | `UInt16` | | Must be `"MPTokenIssuanceDestroy"` | | `Account` | :heavy_check_mark: | `No` | `String` | `AccountID` | | Account submitting the transaction (must be the issuer) | | `MPTokenIssuanceID` | :heavy_check_mark: | `No` | `String` | `UInt192` | | The MPTID of the issuance to destroy | -| `Flags` | | `No` | `Number` | `UInt32` | `0` | Must be 0 (only universal flags allowed) | +| `Flags` | | `No` | `Number` | `UInt32` | `0` | No transaction-specific flags; only universal flags (e.g. `tfFullyCanonicalSig`) are permitted | ### 3.2.1. Failure Conditions **Static validation**[^mptissuancedestroy-static-validation] -[^mptissuancedestroy-static-validation]: Static validation (preflight): [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp#L10-L13), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp#L16-L19) +[^mptissuancedestroy-static-validation]: Static validation (generic preflight gate): [`invokePreflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/Transactor.h#L460-L470) - `temDISABLED`: [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) amendment is not enabled - `temINVALID_FLAG`: Any non-universal flags specified **Validation against the ledger view**[^mptissuancedestroy-preclaim-validation] -[^mptissuancedestroy-preclaim-validation]: Validation against ledger view (preclaim): [`MPTokenIssuanceDestroy.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp#L22-L42) +[^mptissuancedestroy-preclaim-validation]: Validation against ledger view (preclaim): [`MPTokenIssuanceDestroy.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp#L23-L42) - `tecOBJECT_NOT_FOUND`: `MPTokenIssuance` with specified MPTID does not exist - `tecNO_PERMISSION`: Signing account is not the issuer @@ -400,13 +425,17 @@ The `MPTokenIssuanceDestroy` transaction deletes an MPT issuance. This can only - `OutstandingAmount` is non-zero (tokens still held by holders) - `LockedAmount` is non-zero (tokens locked in escrow) +The `LockedAmount` check is a defensive guard: escrow-locked tokens remain counted in `OutstandingAmount`, so the `OutstandingAmount` check already catches an issuance with locked tokens (this branch is unreachable in practice). + **Validation during doApply**[^mptissuancedestroy-doapply-validation] -[^mptissuancedestroy-doapply-validation]: Validation during doApply: [`MPTokenIssuanceDestroy.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceDestroy.cpp#L45-L61) +[^mptissuancedestroy-doapply-validation]: Validation during doApply: [`MPTokenIssuanceDestroy.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceDestroy.cpp#L45-L59) - `tecINTERNAL`: Signing account is not the issuer - `tefBAD_LEDGER`: Failed to remove issuance from owner directory (indicates ledger corruption) +Both doApply errors are defensive: preclaim already guarantees the issuer matches and the issuance exists, so neither is reachable in normal operation. + ### 3.2.2. State Changes - `MPTokenIssuance` object is **deleted**: @@ -425,6 +454,7 @@ The `MPTokenIssuanceSet` transaction is **sent by the issuer only** to modify mu - Lock/unlock the entire issuance (global lock) - Lock/unlock individual holders - Set/clear the `DomainID` field +- Mutate the metadata, transfer fee, and capability flags (requires the DynamicMPT amendment and the corresponding mutability permissions granted at creation) | Field Name | Required? | Modifiable? | JSON Type | Internal Type | Default Value | Description | |---------------------|:------------------:|:-----------:|:----------------------:|:-------------:|:-------------:|:-----------------------------------------------------------------------------------------------------------------| @@ -466,18 +496,18 @@ These flags are used in the `MutableFlags` field to set or clear capability flag **Behavior**: -- **When `Holder` is NOT specified**: Modifies `MPTokenIssuance` (global lock or DomainID) -- **When `Holder` is specified**: Modifies holder's `MPToken` (individual lock only) +- **When `Holder` is NOT specified**: Modifies the `MPTokenIssuance` (global lock/unlock, `DomainID`, or DynamicMPT mutations: metadata, transfer fee, capability flags) +- **When `Holder` is specified**: Modifies the holder's `MPToken` (individual lock/unlock only) **Lock requirements**: -- Individual lock: Requires `lsfMPTCanLock` on issuance (after SingleAssetVault) +- Lock or unlock (global or individual) requires the issuance to have `lsfMPTCanLock`. ### 3.3.1. Failure Conditions **Static validation**[^mptissuanceset-static-validation] -[^mptissuanceset-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L11-L16), [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L19-L22), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L49-L122) +[^mptissuanceset-static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L32-L37), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L40-L43), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L76-L140) - `temDISABLED`: - [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) amendment is not enabled @@ -500,17 +530,18 @@ These flags are used in the `MutableFlags` field to set or clear capability flag **Validation against the ledger view**[^mptissuanceset-preclaim-validation] -[^mptissuanceset-preclaim-validation]: Validation against ledger view (preclaim): [`checkPermission`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L125-L159), [`preclaim`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L162-L249) +[^mptissuanceset-preclaim-validation]: Validation against ledger view (preclaim): [`checkPermission`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L143-L173), [`preclaim`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L176-L265) -- `terNO_ACCOUNT`: Signing account does not exist +- `terNO_ACCOUNT`: Signing account does not exist (enforced by the base transactor, before `MPTokenIssuanceSet` preclaim) - `tecOBJECT_NOT_FOUND`: - `MPTokenIssuance` does not exist - `Holder` specified but `MPToken` does not exist - `DomainID` specified (non-zero) but domain does not exist - `tecNO_PERMISSION`: - Signing account is not the issuer - - Attempting to lock/unlock without `lsfMPTCanLock` (when [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) is enabled) - - `DomainID` specified but issuance does not have `lsfMPTRequireAuth` + - Attempting to lock/unlock without `lsfMPTCanLock` (when [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) or [DynamicMPT](https://xrpl.org/resources/known-amendments#dynamicmpt) is enabled). When neither amendment is enabled, an issuance without `lsfMPTCanLock` rejects any `MPTokenIssuanceSet` with `tecNO_PERMISSION` + - `DomainID` field is present (to set, or to clear with `DomainID` = 0) but the issuance does not have `lsfMPTRequireAuth` + - Clearing `lsfMPTRequireAuth` (via `tmfMPTClearRequireAuth` in `MutableFlags`) while the issuance still has a `DomainID` set. A `DomainID` requires `lsfMPTRequireAuth` to remain active, so the issuer must clear the `DomainID` before clearing `RequireAuth`. - Attempting to change a capability flag (via `MutableFlags`) without the corresponding mutability permission set during issuance creation - Attempting to change `MPTokenMetadata` without `tmfMPTCanMutateMetadata` permission - Attempting to change `TransferFee` without `tmfMPTCanMutateTransferFee` permission @@ -519,7 +550,7 @@ These flags are used in the `MutableFlags` field to set or clear capability flag **Validation during doApply**[^mptissuanceset-doapply-validation] -[^mptissuanceset-doapply-validation]: Validation during doApply: [`MPTokenIssuanceSet.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L252-L338) +[^mptissuanceset-doapply-validation]: Validation during doApply: [`MPTokenIssuanceSet.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L268-L373) - `tecINTERNAL`: `MPTokenIssuance` does not exist @@ -532,12 +563,12 @@ These flags are used in the `MutableFlags` field to set or clear capability flag - If `tfMPTUnlock`: Clear `lsfMPTLocked` flag (global unlock) - If `DomainID` non-zero: Set `DomainID` field - If `DomainID` zero: Clear `DomainID` field (remove from entry) - - If `MPTokenMetadata` present: Update `MPTokenMetadata` field - - If `TransferFee` present: Update `TransferFee` field + - If `MPTokenMetadata` present: update the field, or clear it (remove from the entry) when the value is empty + - If `TransferFee` present: update the field, or clear it (remove from the entry) when the value is 0 (`TransferFee` is `soeDEFAULT`, so absent means 0) - If `MutableFlags` present with set flags: Set corresponding capability flags (e.g., `tmfMPTSetCanTrade` sets `lsfMPTCanTrade`) - If `MutableFlags` present with clear flags: Clear corresponding capability flags (e.g., `tmfMPTClearCanTrade` clears `lsfMPTCanTrade`). Clearing `lsfMPTCanTransfer` via `tmfMPTClearCanTransfer` also clears the `TransferFee` field.[^clear-cantransfer-clears-transferfee] -[^clear-cantransfer-clears-transferfee]: Clearing `lsfMPTCanTransfer` clears `TransferFee`: [`MPTokenIssuanceSet.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenIssuanceSet.cpp#L286-L291) +[^clear-cantransfer-clears-transferfee]: Clearing `lsfMPTCanTransfer` clears `TransferFee`: [`MPTokenIssuanceSet.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenIssuanceSet.cpp#L313-L318) **When `Holder` is specified** (modifying `MPToken`): @@ -580,11 +611,13 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. | Present | Not set | Issuer | Set `lsfMPTAuthorized` flag | | Present | Set | Issuer | Clear `lsfMPTAuthorized` flag | +Issuer-initiated authorize/unauthorize only applies when the issuance has `lsfMPTRequireAuth`; otherwise it fails with `tecNO_AUTH`. + ### 3.4.1. Failure Conditions **Static validation**[^mptokenauthorize-static-validation] -[^mptokenauthorize-static-validation]: Static validation (preflight): [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp#L11-L14), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp#L17-L23) +[^mptokenauthorize-static-validation]: Static validation (preflight): [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp#L23-L26), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp#L29-L35) - `temDISABLED`: [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) amendment is not enabled - `temINVALID_FLAG`: Invalid flags specified @@ -592,13 +625,13 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. **Validation against the ledger view**[^mptokenauthorize-preclaim-validation] -[^mptokenauthorize-preclaim-validation]: Validation against ledger view (preclaim): [`MPTokenAuthorize.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/MPTokenAuthorize.cpp#L26-L128) +[^mptokenauthorize-preclaim-validation]: Validation against ledger view (preclaim): [`MPTokenAuthorize.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/MPTokenAuthorize.cpp#L37-L142) **When Holder NOT specified (holder-initiated)**: - If `tfMPTUnauthorize`: - `tecOBJECT_NOT_FOUND`: `MPToken` does not exist - - `tefINTERNAL`: `MPTokenIssuance` does not exist (and `MPToken` has a positive balance or locked amount) + - `tefINTERNAL`: `MPToken` has a non-zero balance or locked amount but its `MPTokenIssuance` cannot be found (internal inconsistency; should not occur) - `tecHAS_OBLIGATIONS`: - `MPTAmount` is non-zero (cannot delete with balance) - `LockedAmount` is non-zero (cannot delete with locked MPTs) @@ -615,12 +648,13 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. - `MPTokenIssuance` does not exist - `MPToken` does not exist (must be created by holder first) - `tecNO_PERMISSION`: Signing account is not the issuer +- `tecNO_PERMISSION`: `Holder` is a pseudo-account (Vault, LoanBroker, or AMM); pseudo-accounts are implicitly always authorized and cannot be unauthorized - `tecNO_AUTH`: Issuance does not have `lsfMPTRequireAuth` flag (authorization not required) **Validation during doApply**[^mptokenauthorize-doapply-validation] -[^mptokenauthorize-doapply-validation]: Validation during doApply: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1297-L1402) +[^mptokenauthorize-doapply-validation]: Validation during doApply: [`MPTokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L148-L268) - `tecINTERNAL`: Signing account does not exist @@ -633,7 +667,7 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. - `tecINTERNAL`: - Failed to remove `MPToken` from owner directory (indicates ledger corruption) - - `MPToken` does not exist or the balance is not 0 + - `MPToken` does not exist, the balance is not 0, or (with `fixCleanup3_1_3`) the locked amount is not 0 ### 3.4.2. State Changes @@ -647,7 +681,7 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. - `Flags`: Set to 0 (not authorized initially) - Holder's `AccountRoot` is **modified**: - - `OwnerCount`: Incremented by 1 (if OwnerCount >= 2) + - `OwnerCount`: Incremented by 1 - `DirectoryNode` is **created or modified**: - `MPToken` is added to holder's owner directory @@ -658,7 +692,7 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. - Entry is removed from the ledger - Holder's `AccountRoot` is **modified**: - - `OwnerCount`: Decremented by 1 (if it was incremented at creation) + - `OwnerCount`: Decremented by 1 - `DirectoryNode` is **modified**: - `MPToken` is removed from holder's owner directory @@ -675,7 +709,7 @@ The `MPTokenAuthorize` transaction manages `MPToken` entries and authorization. ## 3.5. Clawback Transaction with MPTs -The `Clawback` transaction allows issuers to claw back MPTs from holders when the `lsfMPTCanClawback` flag is set. This is the same transaction type used for [trust line](../trust_lines/README.md#321-clawback-transaction) tokens, but with MPT-specific handling. +The `Clawback` transaction allows issuers to claw back MPTs from holders when the `lsfMPTCanClawback` flag is set. This is the same transaction type used for [trust line](../trust_lines/README.md#312-clawback-transaction) tokens, but with MPT-specific handling. **Fields**: @@ -693,15 +727,14 @@ Transaction fields are described in [Clawback Fields](https://xrpl.org/docs/refe - `Holder` field is not specified (required for MPT clawback) - `Account` equals `Holder` (cannot claw back from self) - `temBAD_AMOUNT`: - - `Amount` is negative or zero + - `Amount` is zero, negative, or greater than the maximum MPT amount (`kMaxMpTokenAmount` = `0x7FFFFFFFFFFFFFFF`) -[^clawback-static-validation]: Static validation (preflight): [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Clawback.cpp#L60-L63), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Clawback.cpp#L66-L75) +[^clawback-static-validation]: Static validation (preflight): [`preflightHelper`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/Clawback.cpp#L56-L76) **Validation against the ledger view:**[^clawback-preclaim-validation] - `terNO_ACCOUNT`: issuer's or holder's account does not exist. -- `tecAMM_ACCOUNT`: If holder's account is an AMM account, such as a SingleAssetVault or AMM account and if - amendment [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) is not enabled. +- `tecAMM_ACCOUNT`: Holder account is an AMM account (has an `sfAMMID` field) and the [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) amendment is not enabled (when SingleAssetVault is enabled, this case is reported as `tecPSEUDO_ACCOUNT` instead). - `tecPSEUDO_ACCOUNT`: If `SingleAssetVault` is enabled and holder's account is any pseudo-account. - `tecOBJECT_NOT_FOUND`: - `MPTokenIssuance` does not exist @@ -711,101 +744,89 @@ Transaction fields are described in [Clawback Fields](https://xrpl.org/docs/refe - Issuance does not have `lsfMPTCanClawback` flag - `tecINSUFFICIENT_FUNDS`: Holder's `MPTAmount` is zero (nothing to claw back) -[^clawback-preclaim-validation]: Validation against the ledger view (preclaim): [`preclaim`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Clawback.cpp#L176-L202) +[^clawback-preclaim-validation]: Validation against the ledger view (preclaim): [`preclaim`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/Clawback.cpp#L184-L211), [`preclaimHelper`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/Clawback.cpp#L150-L182) ### 3.5.2. State Changes - Holder's `MPToken` is **modified**:[^clawback-doapply] - - `MPTAmount`: Decreased by clawed back amount (or to zero if amount exceeds balance) - - If `MPTAmount` becomes zero and all other fields are default, the entry may be deleted - -- Holder's `MPToken` may be **deleted**: - - If balance is zero, locked amount is zero, and no non-default flags are set - - Holder's `OwnerCount` is decremented + - `MPTAmount`: Decreased by the clawed-back amount (capped at the holder's balance) - `MPTokenIssuance` is **modified**: - - `OutstandingAmount`: Decreased by clawed back amount + - `OutstandingAmount`: Decreased by the clawed-back amount -[^clawback-doapply]: Validation during doApply: [`applyHelper`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Clawback.cpp#L239-L263) +The holder's `MPToken` is **not** deleted by clawback, even when its `MPTAmount` reaches zero, and the holder's `OwnerCount` is unchanged. (This differs from trust-line clawback.) + +[^clawback-doapply]: State changes during doApply: [`applyHelper`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/Clawback.cpp#L243-L267) **Notes**: - The clawed back amount is removed from circulation (burned), not transferred to the issuer's balance. If the requested clawback amount exceeds the holder's balance, only the available balance is clawed back (no error). - Transfer fees are waived during clawback. The full clawed back amount is burned from the holder's balance without deducting any transfer fee, even if a `TransferFee` is set on the issuance. +- Clawback ignores lock, freeze, and authorization state: the clawable balance is computed with `IgnoreFreeze` and `IgnoreAuth`, so an issuer can claw back from a locked or unauthorized holder. ## 3.6. MPT Validation Functions -MPT operations are validated through two public functions that call a common underlying validation logic. These functions are used by various transactions (OfferCreate, Payment, AMMCreate, etc.) to ensure MPTs meet the requirements for their intended use. +MPTs are validated for DEX and transfer operations through helper functions in `MPTokenHelpers`. The two primary checks are **`canTrade`** (DEX tradability) and **`canTransfer`** (transferability between two parties), with a convenience wrapper **`canMPTTradeAndTransfer`** that runs both. All three return `tesSUCCESS` for XRP and IOU assets, they only constrain MPTs. + +These checks cover *tradability* and *transferability* only. Two related concerns are validated separately: lock status by `isFrozen` (`lsfMPTLocked`), and holder authorization by `requireAuth` (`lsfMPTRequireAuth`/`lsfMPTAuthorized` and DomainID credentials). A typical call site composes them, e.g. `requireAuth(...)` together with `canTrade(...)`. + +### 3.6.1. canTrade -### 3.6.1. checkMPTDEXAllowed +`canTrade(view, asset)`: checks whether an asset may be traded on the DEX.[^mpt-cantrade] -Used for DEX operations (offers, cross-currency payments). +[^mpt-cantrade]: [`canTrade`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L581-L619) -**Transactions using this validation**: -- OfferCreate (CreateOffer::preclaim) -- CheckCash (when cashing checks with MPT amounts) -- Payment (when using paths for cross-currency conversion - validated in BookStep and MPTEndpointStep) +**Used by**: OfferCreate (`OfferCreate::preclaim`) and cross-currency payment book steps (`BookStep`, `MPTEndpointStep`). AMM transactions reach it indirectly via `canMPTTradeAndTransfer`. -**Validation checks performed**: +**Checks performed** (MPT assets only; IOU/XRP return `tesSUCCESS`): -1. **MPTokenIssuance exists**: Verifies the MPTokenIssuance object exists in the ledger -2. **Issuer account exists**: Verifies the issuer's account exists -3. **Global lock check**: Ensures `lsfMPTLocked` flag is not set on MPTokenIssuance -4. **DEX permission**: Verifies `lsfMPTCanTrade` flag is set (required for all DEX operations) -5. **Transfer permission** (when neither party is issuer): Verifies `lsfMPTCanTransfer` flag is set -6. **Individual lock check** (if holder has MPToken): Ensures `lsfMPTLocked` flag is not set on the holder's MPToken entry +1. **MPTokenIssuance exists**, else `tecOBJECT_NOT_FOUND` +2. **`lsfMPTCanTrade` flag set** on the issuance, else `tecNO_PERMISSION` +3. **Vault shares**: recurses into the underlying asset's tradability via `sfReferenceHolding` (when the `fixCleanup3_2_0` amendment is enabled) **Possible error codes**: -- `tecNO_ISSUER`: Issuer account does not exist - `tecOBJECT_NOT_FOUND`: MPTokenIssuance does not exist -- `tecLOCKED`: Global lock (`lsfMPTLocked` on MPTokenIssuance) OR individual lock (`lsfMPTLocked` on holder's MPToken) -- `tecNO_PERMISSION`: `lsfMPTCanTrade` flag not set OR `lsfMPTCanTransfer` flag not set (when neither party is issuer) +- `tecNO_PERMISSION`: `lsfMPTCanTrade` flag not set -**When `lsfMPTCanTransfer` is checked**: +`canTrade` does **not** check lock status or holder authorization. Those are handled separately by `isFrozen` and `requireAuth`. -The `lsfMPTCanTransfer` check is only performed when the account is not the issuer AND (there is no destination account OR the destination is not the issuer). This means: -- Issuer-involved operations (minting/burning) do not require `lsfMPTCanTransfer` -- Holder-to-holder operations require both `lsfMPTCanTrade` (for DEX) and `lsfMPTCanTransfer` +### 3.6.2. canTransfer -### 3.6.2. checkMPTTxAllowed +`canTransfer(view, mptIssue, from, to, waive = No)` checks whether `to` may receive the MPT from `from`.[^mpt-cantransfer] -Used for transactions involving MPTs that are not strictly DEX operations. +[^mpt-cantransfer]: [`canTransfer`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L524-L579) -**Transactions using this validation**: -- CheckCreate (when creating checks with MPT amounts) -- AMMCreate, AMMDeposit, AMMWithdraw (when involving MPT assets) +**Used by**: holder-to-holder MPT payments (`MPTEndpointStep`, `Payment`), `CheckCash`/`CheckCreate`, `EscrowCreate`, Vault and Lending transactions, and offer-owner validation in `BookStep`. -**Validation checks performed**: +**Passes when any of**: +- `waive` is `WaiveMPTCanTransfer::Yes` (recovery paths, e.g. unwinding vault or lending positions after transferability is revoked) +- `from` or `to` is the issuer (issuer-involved minting/burning never requires `lsfMPTCanTransfer`) +- **`lsfMPTCanTransfer` flag set** on the issuance -Same as `checkMPTDEXAllowed` but the validation logic uses the actual transaction type instead of treating it as a DEX operation. The key difference is: -- For DEX operations: Always treats the operation as requiring DEX permissions -- For other transactions: Validates based on the specific transaction type's requirements +Otherwise returns `tecNO_AUTH`. Vault shares recurse into the underlying asset's transferability (when the `fixCleanup3_2_0` amendment is enabled). **Possible error codes**: -Same as `checkMPTDEXAllowed`: -- `tecNO_ISSUER` -- `tecOBJECT_NOT_FOUND` -- `tecLOCKED` -- `tecNO_PERMISSION` +- `tecOBJECT_NOT_FOUND`: MPTokenIssuance does not exist +- `tecNO_AUTH`: `lsfMPTCanTransfer` flag not set and neither party is the issuer + +### 3.6.3. canMPTTradeAndTransfer -**Note**: Both functions call the same underlying `checkMPTAllowed()` function, which contains the core validation logic. The difference is in how they categorize the operation (DEX vs non-DEX) which affects whether `lsfMPTCanTrade` is required. +`canMPTTradeAndTransfer(view, asset, from, to)`: convenience wrapper that runs `canTrade` then `canTransfer`, returning the first failure (or `tesSUCCESS` immediately for non-MPT assets).[^mpt-canmpttradetransfer] + +[^mpt-canmpttradetransfer]: [`canMPTTradeAndTransfer`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L621-L635) + +**Used by**: AMM transactions (`AMMCreate`, `AMMDeposit`, `AMMWithdraw`), which require both DEX tradability and transferability. # 4. MPT Payment Execution MPT payments transfer Multi-Purpose Tokens between accounts. Depending on whether the [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) amendment is enabled, these payments execute through one of two paths: a direct transfer mechanism (when MPTokensV2 is not enabled) or the [Flow engine](../flow/README.md) (when MPTokensV2 is enabled). Both execution paths use the same underlying transfer logic. -**Transfer mechanics**: The three transfer scenarios described below apply regardless of which execution path is used, as both ultimately call the same `rippleCreditMPT` function to modify ledger entries. The transfer behavior depends on whether the issuer is involved in the transaction. +**Transfer mechanics**: The three transfer scenarios described below apply regardless of which execution path is used, as both ultimately reach the same MPT transfer logic in `TokenHelpers.cpp` (`accountSendMPT`, which applies any transfer fee and then mutates the ledger entries via `directSendNoFeeMPT`). The transfer behavior depends on whether the issuer is involved in the transaction. **Prerequisites**: Both source and destination must have `MPToken` entries (created via `MPTokenAuthorize` transaction) unless one party is the issuer. The issuer never has an `MPToken` entry. -If the `MPTokenIssuance` has a `DomainID` field set, the receiving account must have valid, non-expired credentials for that PermissionedDomain to receive the MPT (see [Domain Membership](../permissioned_domains/README.md#41-domain-membership) for credential verification details). This authorization is checked during payment preclaim: -- If the receiver has an existing `MPToken` entry with the `lsfMPTAuthorized` flag set, the payment succeeds (authorization previously granted) -- If the receiver has no `MPToken` entry or an unauthorized `MPToken` entry, the ledger checks for valid credentials matching the `MPTokenIssuance.DomainID` -- Valid credentials automatically authorize the receiver, and an `MPToken` entry is created if needed during doApply -- The sender does not need domain credentials - only the receiver's authorization is checked - -This domain-based authorization is **separate** from PermissionedDEX domain restrictions on offers. The `MPTokenIssuance.DomainID` controls who can hold the MPT, while `Offer.DomainID` controls which order book an offer is placed in. See [MPT DomainID and Authorization](#11-domainid-and-authorization) and [PermissionedDomains documentation](../permissioned_domains/README.md) for detailed explanation of the distinction. +If the `MPTokenIssuance` has a `DomainID` set, the receiving account must be authorized for that domain to receive the MPT (see [MPT DomainID and Authorization](#11-domainid-and-authorization)). Authorization is verified during payment processing via `requireAuth` (in `doApply` for the direct path, and in the strand step for the Flow path): the receiver passes if its existing `MPToken` has `lsfMPTAuthorized` set, or if it holds valid, non-expired credentials for the issuance's `DomainID`. `requireAuth` does not create an `MPToken`; the receiver must already have one (see Prerequisites above). Only the receiver's authorization is checked, not the sender's. ## 4.1. Transfer Scenarios @@ -834,6 +855,10 @@ sequenceDiagram **Net effect**: Total supply increases by the sent amount. +Minting fails with `tecPATH_DRY` if it would push `OutstandingAmount` above the issuance's `MaximumAmount` (which defaults to `kMaxMpTokenAmount` when unset).[^mpt-mint-cap] + +[^mpt-mint-cap]: [`directSendNoFeeMPT`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L1076-L1087) + ### 4.1.2. Holder Burning (Holder -> Issuer) When a holder sends MPTs back to the issuer, tokens are burned from circulation: @@ -882,7 +907,7 @@ sequenceDiagram Note over IS: OutstandingAmount: 1000 -> 999
(net: -1 fee burned) ``` -1. Calculate transfer fee: `actualAmount = amount * (1 + transferFee%)` +1. Calculate the transfer fee: `actualAmount = amount * transferRate`, where `transferRate = 1 + sfTransferFee / 100000` (`sfTransferFee` is in tenths of a basis point: 1000 = 1%, 50000 = 50%) 2. **Credit step** (through issuer to receiver): - Increase `OutstandingAmount` on `MPTokenIssuance` by delivery amount - Increase `MPTAmount` on receiver's `MPToken` by delivery amount @@ -894,3 +919,18 @@ sequenceDiagram - Receiver gets: delivery amount - Sender pays: delivery amount + fee - Total supply decreases by: fee amount (burned from circulation) + +## 4.2. Canonical MPT Amount Validation + +Under the `fixCleanup3_2_0` amendment, every transaction is checked at preflight for canonical MPT amounts. An MPT amount is canonical only when it is non-negative, has a zero exponent, and its mantissa does not exceed `kMaxMpTokenAmount` (`0x7FFFFFFFFFFFFFFF`). A transaction carrying a non-canonical MPT amount in any field is rejected with `temBAD_AMOUNT`. This applies to every MPT-bearing transaction, not just payments.[^mpt-canonical] + +A `ValidAmounts` transaction invariant provides defense in depth: it rejects any ledger entry left with a non-canonical MPT (or XRP) amount after a transaction applies.[^mpt-validamounts] + +[^mpt-canonical]: [`preflightUniversal`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/Transactor.cpp#L260-L265), [`isLegalMPT`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STAmount.h#L603-L614) +[^mpt-validamounts]: [`InvariantCheck.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/invariants/InvariantCheck.cpp#L1083-L1110) + +## 4.3. Locks under the Flow Path + +When MPTokensV2 is enabled, MPT payments run through the Flow engine and lock/freeze is enforced per Flow step in `MPTEndpointStep`. A global lock (`lsfMPTLocked` on the `MPTokenIssuance`) is checked on the first step, and an individual lock (`lsfMPTLocked` on a holder's `MPToken`) on each step; a locked step returns `terLOCKED`. A pure issuer-side transfer (minting or burning) is a single Flow step (both first and last) and is exempt from the freeze check, so it succeeds even when the MPT is globally locked.[^mpt-flow-lock] + +[^mpt-flow-lock]: [`MPTEndpointStep.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/MPTEndpointStep.cpp#L843-L856) diff --git a/docs/offers/README.md b/docs/offers/README.md index 69d7d9b..147d52b 100644 --- a/docs/offers/README.md +++ b/docs/offers/README.md @@ -48,12 +48,11 @@ When `rippled` applies an `OfferCreate`, it first invokes the payment [flow engi 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. +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 -Keeping in mind that offers are actually limit orders, once they are placed, offers can immediately be executed, -partially or fully. +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 @@ -62,9 +61,9 @@ the [default path](../path_finding/README.md#24-default-paths). If neither taker 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 ImmediateOrCancel it will never be placed in the order book. It can be fully or partially filled during crossing, but `Offer` ledger entry will never be created for it. +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. +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. @@ -100,18 +99,17 @@ flowchart LR **Atomicity:** -Offer crossing uses two `Sandbox` instances to ensure atomic state application: -- `sb`: Contains all state changes (fee payment, offer deletions, crossing results, new offer placement) -- `sbCancel`: Contains only fee payment and unfunded offer deletions +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`: only the cleanup of unfunded or expired offers encountered during crossing -For `tfFillOrKill` offers that don't fully cross, `sbCancel` is applied instead of `sb`, ensuring the offer crossing is rolled back while preserving fee payment. See [Ledger -Views and Sandboxes](../transactions/README.md#5-ledger-views-and-sandboxes) for details on how sandboxes provide atomic state changes. +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 the cleanup of unfunded or expired offers. See [Ledger Views and Sandboxes](../transactions/README.md#5-ledger-views-and-sandboxes) for how sandboxes provide atomic state changes. ### 1.2.1. 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 `rippled` implementation, `takerPays` is capped at `STAmount::cMaxNative` for XRP[^cMaxNative], `STAmount::cMaxValue / 2` for IOUs[^cMaxValue-iou], and `maxMPTokenAmount / 2` for MPTs[^maxMPTokenAmount-mpt]. +When selling, the offer will accept more than the specified `takerPays` amount to maximize the sale of `takerGets`. In the `rippled` 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. @@ -122,11 +120,11 @@ Bob is willing to buy 10 USD for his 100 XRP. He does not want to buy more than **Offers:** -- Alice sends a **sell** offer: +- Alice's **sell** offer rests on the book first: - Taker pays 100 XRP - Taker gets 20 USD -- Bob sends a **buy** offer +- Bob then submits a **buy** offer, which crosses Alice's resting offer: - Taker pays 10 USD - Taker gets 100 XRP @@ -141,11 +139,11 @@ Alice is willing to buy 100 XRP for her 20 USD. Bob is willing to sell his 100 X **Offers:** -- Alice sends a **buy** offer: +- Alice's **buy** offer rests on the book first: - Taker pays 100 XRP - Taker gets 20 USD -- Bob sends a **sell** offer +- Bob then submits a **sell** offer, which crosses Alice's resting offer: - Taker pays 10 USD - Taker gets 100 XRP @@ -154,76 +152,90 @@ Alice is willing to buy 100 XRP for her 20 USD. Bob is willing to sell his 100 X - 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STAmount.h#L52) -[^cMaxValue-iou]: IOU maximum value halved for transfer rate: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L417-L420) -[^maxMPTokenAmount-mpt]: MPT maximum amount halved for transfer rate: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L423), maximum defined in [`Protocol.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/Protocol.h#L99) +[^cMaxNative]: XRP maximum native value: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STAmount.h#L55) +[^cMaxValue-iou]: IOU maximum value halved for transfer rate: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L440), maximum defined in [`Protocol.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/account/AccountSet.cpp#L128-L131) ### 1.2.2. 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 -- 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] +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: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L394-L396) +[^auto-bridging-path]: Auto-bridging path construction: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L410-L412) +[^passive-threshold]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L394-L397) ### 1.2.3. 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`. +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 [BookStep transfer rates](../flow/steps.md#44-helper-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 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 the pre-crossing exchange rate +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 the pre-crossing exchange rate +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 -- 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] +- 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: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L511-L518) -[^sell-offer-residual]: Sell offer residual calculation: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L486-L504) -[^no-balance-no-offer]: No balance check after crossing: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L464-L470) +[^buy-offer-residual]: Buy offer residual calculation: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L527-L533) +[^sell-offer-residual]: Sell offer residual calculation: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L480-L486) +[^offer-reserve]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L834-L844) ## 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 (64-bit unsigned integer), IOUs (48-bit mantissa with sign and exponent), and MPTs (64-bit unsigned integer). +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] -`rippled` implementation normalizes the rate by packing the result into a 64-bit integer: +`rippled` implementation normalizes the rate by packing the result into a 64-bit integer[^rate-packing]: - Upper 8 bits: exponent + 100 - Lower 56 bits: mantissa -In any case of an exception, like an overflow, it will return the rate `0` and not store the offer. +`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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STAmount.h#L55) +[^repr-iou]: [`STAmount.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STAmount.h#L47-L53) +[^repr-mpt]: [`Protocol.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/Protocol.h#L234) +[^rate-packing]: [`STAmount.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. -If `TickSize` is present, all offers are rounded by truncating the rate to the specified `TickSize`. If two issuers in the offer -have different `TickSize` set, the smaller is used. If only one has `TickSize` set, that is used. XRP never has a `TickSize`. TickSize is read from the issuer's account and applies to both IOUs and MPTs. +`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 -After the rate is rounded, one side of the offer is adjusted to maintain the rounded rate: -- For **sell offers** (`tfSell` flag): `takerPays` is recalculated based on the rounded rate -- For **buy offers** (no `tfSell` flag): `takerGets` is recalculated based on the rounded rate +[^ticksize-range]: [`Quality.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/Quality.h#L97-L98) +[^ticksize-round]: [`Quality.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Quality.cpp#L134-L162) +[^ticksize-integral]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L668-L700) ## 1.4. Offer deletion @@ -237,19 +249,24 @@ If `OfferSequence` is provided: 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. + ## 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. +**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. +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). +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] + +[^domain-book-segregation]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Indexes.cpp#L102-L110) +[^hybrid-books]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L560-L602) # 2. Ledger Entries @@ -266,13 +283,13 @@ When an offer is created, it stores references to both its book directory (`sfBo **Order Book Segregation:** -The XRP Ledger maintains separate order book directories based on domain participation: +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(BOOK_NAMESPACE, asset_in, asset_out)`. Contains open offers and hybrid offers (via AdditionalBooks references). All accounts can create open offers. +- **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(BOOK_NAMESPACE, 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. +- **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. When a hybrid offer is created, it only crosses with offers in the domain book. +- **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 @@ -293,19 +310,14 @@ in [Offer Fields](https://xrpl.org/docs/references/protocol/ledger-data/ledger-e #### 2.1.2.1. Asset-Specific Fields -Offers can involve three types of assets, each using different fields for identification: - -**IOU Assets**: -- `TakerPaysCurrency`, `TakerPaysIssuer` (when `TakerPays` is a IOU) -- `TakerGetsCurrency`, `TakerGetsIssuer` (when `TakerGets` is a IOU) +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 -**MPT Assets**: -- `TakerPaysMPT` (UInt192 MPTID when `TakerPays` is an MPT) -- `TakerGetsMPT` (UInt192 MPTID when `TakerGets` is an MPT) +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. -**XRP**: Uses `TakerPays`/`TakerGets` Amount fields directly. - -An offer stores only the fields relevant to its asset types. All combinations of XRP, IOUs, and MPTs are supported. +[^offer-asset-amounts]: [`ledger_entries.macro`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/ledger_entries.macro#L227-L240) #### 2.1.2.2. Domain-Specific Fields @@ -317,6 +329,14 @@ An offer stores only the fields relevant to its asset types. All combinations of 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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L944-L953) + #### 2.1.2.3. Flags Flags are described @@ -335,7 +355,7 @@ The offer is stored in the ledger and tracked in an Owner Directory owned by the 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 +- 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. @@ -355,7 +375,7 @@ The first 192 bits are the first 192 bits of [SHA512-Half](https://xrpl.org/docs 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/protocol/Indexes.cpp#L96-L143) +[^book-dir-hash]: Book directory hash computation: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -378,8 +398,8 @@ The key is the result of [SHA512-Half](https://xrpl.org/docs/references/protocol Each directory can span multiple pages to accommodate large numbers of entries: -- **Maximum entries per page**: 32 offers -- **Maximum pages per directory**: 262,144 pages +- **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 @@ -387,18 +407,22 @@ Pages form a doubly-linked list structure: - `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 IDs (see [2.2.1](#221-object-identifier)) and are linked to the root via `sfRootIndex` and to adjacent pages via `sfIndexNext`/`sfIndexPrevious` +- 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 appropriate directory +- 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 262,144 pages, the transaction fails with `tecDIR_FULL` +- 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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/ApplyView.cpp#L124-L129) +[^page-keylet]: [`Indexes.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/protocol/Indexes.cpp#L362-L369) +[^dir-append-insert]: [`ApplyView.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/ApplyView.h#L301-L354) + ### 2.2.3. Fields **Common Fields** (all directories): @@ -408,11 +432,11 @@ Pages form a doubly-linked list structure: - `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 a IOU) -- `sfTakerPaysIssuer`: Issuer account ID for takerPays (when asset is a IOU) +- `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 a IOU) -- `sfTakerGetsIssuer`: Issuer account ID for takerGets (when asset is a IOU) +- `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 @@ -452,11 +476,12 @@ For this reason, certain `tec` outcomes are covered in the [state changes](#3112 - 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](#2121-flags). + - 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`. @@ -470,12 +495,10 @@ For this reason, certain `tec` outcomes are covered in the [state changes](#3112 **Validation against the ledger view:** - `terNO_ACCOUNT`: signing account does not exist -- `tecFROZEN`: either `takerPays` or `takerGets` involves an account with `lsfGlobalFreeze` flag, fail with `tecFROZEN`. An offer cannot be created for a frozen issuer. +- `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 bigger than the sequence number of the next valid transaction for the signing account. -- Transaction's `Expiration` field is before the close time of previously closed ledger: - - `tecEXPIRED`: [DepositAuth](https://xrpl.org/resources/known-amendments#depositauth) is enabled. - - `tesSUCCESS`: [DepositAuth](https://xrpl.org/resources/known-amendments#depositauth) is not enabled. +- `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 @@ -498,52 +521,53 @@ For this reason, certain `tec` outcomes are covered in the [state changes](#3112 - `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 [`checkMPTDEXAllowed`](../mpts/README.md#361-checkmptdexallowed). See [MPT Validation Functions](../mpts/README.md#36-mpt-validation-functions) for complete details on validation logic and error conditions. +**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: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L286-L287) -[^checkAcceptAsset-mpt-auth]: MPT authorization via requireAuth with WeakAuth: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L319-L323), [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L2646-L2670) +[^checkAcceptAsset-noauth]: Unauthorized trust line returns auth errors: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L330-L340), [`View.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/MPTokenHelpers.cpp#L360-L384) +[^domainid-zero]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L99-L101) +[^domain-cancel-regular]: [`PermissionedDEXInvariant.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L41-L43), [finalize](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/invariants/PermissionedDEXInvariant.cpp#L105-L111) +[^offer-expired]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L222-L228), [doApply](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L651-L657) +[^offer-bad-seq]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L215-L221) +[^offercancel-bad-seq]: [`OfferCancel.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCancel.cpp#L34-L46) +[^fok-killed]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L807-L812), [`features.macro`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/features.macro#L100) +[^ioc-killed]: [`OfferCreate.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L815-L825), [`features.macro`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/detail/features.macro#L132) +[^global-frozen]: [`TokenHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/TokenHelpers.cpp#L59-L65) **Validation during doApply:** -- Transaction's `Expiration` field is before the close time of previously closed ledger: - - `tecEXPIRED`: [DepositAuth](https://xrpl.org/resources/known-amendments#depositauth) is enabled. - - `tesSUCCESS`: [DepositAuth](https://xrpl.org/resources/known-amendments#depositauth) is not enabled (fails and returns). +- `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] - `Offer` object is **not created**: - - If offer is not fully crossed and it was submitted with `tfFillOrKill` flag, fail and: - - If [fix1578](https://xrpl.org/resources/known-amendments#fix1578) is enabled, return `tecKILLED`. - - If [fix1578](https://xrpl.org/resources/known-amendments#fix1578) is not enabled, return `tesSUCCESS`. - - If offer is not at all crossed and it was submitted with `tfImmediateOrCancel` flag, fail and: - - If [ImmediateOfferKilled](https://xrpl.org/resources/known-amendments#immediateofferkilled) is enabled, return - `tecKILLED`. - - If [ImmediateOfferKilled](https://xrpl.org/resources/known-amendments#immediateofferkilled) is not enabled, - return `tesSUCCESS`. + - 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 balance is down to 0 after partial filling. + - 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**: - - If `Offer` is not fully crossed. + - 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`. - - If the book directory root page does not exist, it is created with these fields: + - 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 262,144 pages, the transaction fails with `tecDIR_FULL` + - 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): @@ -569,7 +593,7 @@ in [OfferCancel Fields](https://xrpl.org/docs/references/protocol/transactions/t **Validation against the ledger view:** - `terNO_ACCOUNT`: signing account does not exist -- `temBAD_SEQUENCE`: `OfferSequence` is bigger than the sequence number of the next valid transaction for the signing account +- `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 diff --git a/docs/path_finding/README.md b/docs/path_finding/README.md index 0976db6..f190ca0 100644 --- a/docs/path_finding/README.md +++ b/docs/path_finding/README.md @@ -7,7 +7,7 @@ - [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, Different Issuers](#116-example-same-currency-code-different-issuers) + - [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) @@ -219,7 +219,7 @@ Starting from Alice with 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L238-L241) +[^source-issuer-default]: Source issuer defaults to source account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L274-L277) **Step 4: Expand Path Type: `"saxfd"`** - `"s"`: Start at Alice (USD) @@ -279,11 +279,11 @@ Unless the user is trying to convert the entire possible amount of an asset, whe 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L139-L143) +[^min-liquidity]: Minimum liquidity calculation: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L182-L186) -[^max-paths]: Maximum paths constant: [`TransactionSign.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/rpc/detail/TransactionSign.cpp#L286-L291) +[^max-paths]: Maximum paths constant: [`TransactionSign.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/TransactionSign.cpp#L317-L321) -[^rank-sort]: Path ranking comparator: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L559-L574) +[^rank-sort]: Path ranking comparator: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L582-L597) ## 1.5. Structure @@ -332,9 +332,9 @@ Path finding supports an optional `domain` parameter that enables permissioned D - 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:180,192`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L180-L192) -[^pathfinder-domain-orderbook]: OrderBookDB domain filtering flow: `addLink` ([`Pathfinder.cpp:1002`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1002)) calls `getPathsOut` ([`Pathfinder.cpp:1161`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1161)), which queries OrderBookDB with domain parameter ([`Pathfinder.cpp:763`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L763)) -[^pathfinder-domain-flow]: Domain passed to Flow: [`Pathfinder.cpp:380`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L380) +[^pathfinder-domain-constructor]: Pathfinder domain parameter storage: [`Pathfinder.cpp:220,230`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L995)) calls `getPathsOut` ([`Pathfinder.cpp:1145`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1145)), which queries OrderBookDB with domain parameter ([`Pathfinder.cpp:786`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L786)) +[^pathfinder-domain-flow]: Domain passed to Flow: [`Pathfinder.cpp:411`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L411) # 2. Terminology and Concepts @@ -371,25 +371,25 @@ The pathfinder categorizes each payment request into one of five types: | PaymentType | Description | Example | |-------------|------------------------------|----------------------------------------| -| `pt_XRP_to_XRP` | XRP to XRP payment | Alice sends XRP to Bob | -| `pt_XRP_to_nonXRP` | XRP to IOU or MPT payment | Alice sends XRP, Bob receives MPT | -| `pt_nonXRP_to_XRP` | IOU or MPT to XRP payment | Alice sends USD or MPT, Bob receives XRP | -| `pt_nonXRP_to_same` | Same IOU or MPT payment | Alice sends USD, Bob receives USD (same asset) | -| `pt_nonXRP_to_nonXRP` | Different IOU or MPT payment | Alice sends EUR, Bob receives USD | +| `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 `pt_XRP_to_XRP` 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. +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 `pt_XRP_to_nonXRP`: +Example types for `PaymentType::XrpToNonXrp`: | Cost | Type | Path Structure | |------|----------|--------------------------------------------------------------| -| 1 | `sfd` | Source -> Book -> Destination | -| 3 | `sfad` | Source -> Book -> Account -> Destination | -| 5 | `sfaad` | Source -> Book -> Account -> Account -> Destination | +| 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 | @@ -397,7 +397,7 @@ The path finding algorithm searches through these types based on the requested s 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1417-L1480) +[^init-path-table]: Path table initialization: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1388-L1453) **Configuration** @@ -410,35 +410,35 @@ The search depth can be configured in `rippled.cfg`: | `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. +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: -**`nt_SOURCE` (code: `s`)** - The source account +**`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. -**`nt_ACCOUNTS` (code: `a`)** - Accounts connected via trust lines or MPTs +**`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. -**`nt_BOOKS` (code: `b`)** - Order books for currency conversion +**`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. -**`nt_XRP_BOOK` (code: `x`)** - Order book to XRP +**`NodeType::XrpBook` (code: `x`)** - Order book to XRP -A specialized version of `nt_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. +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. -**`nt_DEST_BOOK` (code: `f`)** - Order book to destination currency +**`NodeType::DestBook` (code: `f`)** - Order book to destination currency -This is another specialized version of `nt_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. +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. -**`nt_DESTINATION` (code: `d`)** - The destination account +**`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. @@ -541,12 +541,12 @@ An `STPathElement`[^stpathelement] is the data structure representing a single s - `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STPathSet.h#L18) -[^mtype]: mType field: [`STPathSet.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STPathSet.h#L29) -[^typeaccount]: typeAccount constant: [`STPathSet.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STPathSet.h#L31) -[^typecurrency]: typeCurrency constant: [`STPathSet.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STPathSet.h#L33) -[^typeissuer]: typeIssuer constant: [`STPathSet.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STPathSet.h#L34) -[^typempt]: typeMPT constant: [`STPathSet.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/STPathSet.h#L35) +[^stpathelement]: STPathElement class definition: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STPathSet.h#L17) +[^mtype]: mType field: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STPathSet.h#L19) +[^typeaccount]: typeAccount constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STPathSet.h#L32) +[^typecurrency]: typeCurrency constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STPathSet.h#L33) +[^typeissuer]: typeIssuer constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STPathSet.h#L34) +[^typempt]: typeMPT constant: [`STPathSet.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/STPathSet.h#L35) **Types of path elements:** @@ -581,10 +581,10 @@ Represents an MPT order book. Has both MPT identifier and issuer (0x40 | 0x20 = 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1216-L1220) -[^xrp-book-element-creation]: XRP book element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1242-L1246) -[^iou-book-element-creation]: IOU book element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1315-L1319) -[^source-element-normalization]: Source element added during path normalization: [`PaySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L274-L286) +[^account-element-creation]: Account element creation during pathfinding: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/PaySteps.cpp#L262-L273) ## 3.3. Path @@ -683,7 +683,7 @@ The entire flow can be interrupted by using `continueCallback`. This callback al 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L203-L349) +[^find-paths]: Main path discovery function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -739,7 +739,7 @@ def findPaths(searchLevel, continueCallback) -> bool: issuer = xrpAccount() if isXRP(mSrcPathAsset) else account mSource = STPathElement(account, mSrcPathAsset, issuer) - # Determine payment type (one of: pt_XRP_to_XRP, pt_XRP_to_nonXRP, etc.) + # Determine payment type (one of: PaymentType::XrpToXrp, PaymentType::XrpToNonXrp, etc.) paymentType = determinePaymentType(mSrcPathAsset, mDstAmount.asset()) # Search for paths using types for this payment type @@ -759,7 +759,7 @@ def findPaths(searchLevel, continueCallback) -> bool: `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L856-L946) +[^add-paths-for-type]: Incremental path building function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L872-L943) **Parameters:** @@ -809,17 +809,17 @@ def addPathsForType(pathType, continueCallback) -> list[STPath]: nodeType = pathType[-1] # Get last node type (e.g., 'd' from "sfd") pathsOut = [] - if nodeType == nt_SOURCE: + if nodeType == NodeType::Source: pathsOut = [empty_path] - elif nodeType == nt_ACCOUNTS: + elif nodeType == NodeType::Accounts: addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_ACCOUNTS, continueCallback=continueCallback) - elif nodeType == nt_BOOKS: + elif nodeType == NodeType::Books: addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS, continueCallback=continueCallback) - elif nodeType == nt_XRP_BOOK: + elif nodeType == NodeType::XrpBook: addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS | afOB_XRP, continueCallback=continueCallback) - elif nodeType == nt_DEST_BOOK: + elif nodeType == NodeType::DestBook: addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_BOOKS | afOB_LAST, continueCallback=continueCallback) - elif nodeType == nt_DESTINATION: + elif nodeType == NodeType::Destination: addLinks(currentPaths=parentPaths, incompletePaths=pathsOut, addFlags=afADD_ACCOUNTS | afAC_LAST, continueCallback=continueCallback) mPaths[pathType] = pathsOut @@ -830,7 +830,7 @@ def addPathsForType(pathType, continueCallback) -> list[STPath]: `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L840-L854) +[^add-links]: Wrapper function for batch path extension: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L857-L870) **Parameters:** @@ -855,7 +855,7 @@ def addLinks(currentPaths, incompletePaths, addFlags, continueCallback): `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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1002-L1354) +[^add-link]: Core path expansion function: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L995-L1324) **Parameters:** @@ -868,7 +868,7 @@ def addLinks(currentPaths, incompletePaths, addFlags, continueCallback): 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1008-L1012) +[^addlink-endpoint]: Endpoint extraction from partial path: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -886,7 +886,7 @@ The function's behavior varies significantly between account expansion and book 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1026-L1034) +[^xrp-endpoint-check]: XRP endpoint completion check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -913,40 +913,40 @@ Each asset connection undergoes these checks in order: - 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1092-L1101) -[^dest-bypass]: Destination account bypass check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1079-L1083) -[^dest-only]: Destination-only filtering check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1087-L1090) -[^loop-detection]: Loop detection using hasSeen: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1122-L1123) -[^liquidity-check]: Liquidity and NoRipple check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1102-L1120) -[^source-loop]: Source loop prevention check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1154-L1157) -[^get-ripple-lines-call]: getRippleLines call in addLink: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1177-L1180) -[^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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L963-L986) -[^noripple-direction]: Trust line fetch direction based on NoRipple: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1175-L1183) -[^get-ripple-lines-direction]: LineDirection::incoming excludes trust lines where the account has NoRipple set: [`TrustLine.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/TrustLine.cpp#L57-L58) -[^noripple-candidate-check]: Per-candidate NoRipple check in addLink: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1111) -[^asset-cache-superset]: AssetCache returns the outgoing superset when incoming is requested but outgoing is already cached: [`AssetCache.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/AssetCache.cpp#L67-L76) -[^getpathsout]: getPathsOut computes the paths out score for an account: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L725-L840) -[^getpathsout-auth]: getPathsOut checks lsfRequireAuth on the candidate account: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L747-L752) -[^getpathsout-booksize]: Score starts with order book size: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L763) -[^getpathsout-destination-bonus]: Destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L784-L789) -[^getpathsout-frozen]: Global freeze check in getPathsOut: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L753-L761) -[^getpathsout-iou-loop]: IOU trust line scoring loop: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L766-L803) -[^getpathsout-noripple]: getPathsOut skips trust lines where the peer has NoRipple set: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L791-L793) -[^getpathsout-freeze]: getPathsOut skips trust lines where the peer has frozen the line: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L795-L797) -[^getpathsout-mpt-loop]: MPT scoring loop: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L806-L832) -[^getpathsout-mpt-match]: MPT ID match check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L811-L812) -[^getpathsout-mpt-balance]: MPT zero balance or maxed out check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L814-L815) -[^getpathsout-mpt-auth]: MPT authorization check: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L817-L818) -[^getpathsout-mpt-destination]: MPT destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L820-L823) -[^getpathsout-mpt-frozen]: MPT frozen check (redundant with outer freeze check): [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L824-L825) -[^getpathsout-mpt-count]: MPT count increment: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L828-L829) -[^compare-account-candidate]: compareAccountCandidate sorts by priority descending, then account ID descending: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L65-L81) -[^dest-complete-path]: Destination account with matching asset completes the path: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1134-L1145) -[^dest-high-priority]: Destination account with non-matching asset receives high priority directly: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1147-L1152) -[^getpathsout-zero-filter]: Candidates with score 0 are not added: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1168) -[^candidate-extend]: Selected candidates are extended into incomplete paths: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1210-L1223) -[^mpt-peer-issuer]: MPT peer is always the issuer, extracted from the MPTID: [`Pathfinder.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L1066-L1068), [`MPTIssue.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/MPTIssue.h#L79-L84) -[^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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/detail/PaySteps.cpp#L273-L337) +[^currency-match]: Currency/asset match check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1082-L1092) +[^dest-bypass]: Destination account bypass check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1069-L1073) +[^dest-only]: Destination-only filtering check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1077-L1080) +[^loop-detection]: Loop detection using hasSeen: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1110) +[^liquidity-check]: Liquidity and NoRipple check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1093-L1108) +[^source-loop]: Source loop prevention check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1138-L1141) +[^get-ripple-lines-call]: getRippleLines call in addLink: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/TrustLine.cpp#L61) +[^noripple-candidate-check]: Per-candidate NoRipple check in addLink: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L749-L854) +[^getpathsout-auth]: getPathsOut checks lsfRequireAuth on the candidate account: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L771-L775) +[^getpathsout-booksize]: Score starts with order book size: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L786) +[^getpathsout-destination-bonus]: Destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L802-L806) +[^getpathsout-frozen]: Global freeze check in getPathsOut: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L776-L784) +[^getpathsout-iou-loop]: IOU trust line scoring loop: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L789-L820) +[^getpathsout-noripple]: getPathsOut skips trust lines where the peer has NoRipple set: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L808-L810) +[^getpathsout-freeze]: getPathsOut skips trust lines where the peer has frozen the line: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L812-L814) +[^getpathsout-mpt-loop]: MPT scoring loop: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L823-L849) +[^getpathsout-mpt-match]: MPT ID match check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L828-L829) +[^getpathsout-mpt-balance]: MPT zero balance or maxed out check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L831-L832) +[^getpathsout-mpt-auth]: MPT authorization check: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L834-L835) +[^getpathsout-mpt-destination]: MPT destination bonus of +10000: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L837-L840) +[^getpathsout-mpt-frozen]: MPT frozen check (redundant with outer freeze check): [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L841) +[^getpathsout-mpt-count]: MPT count increment: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L845-L846) +[^compare-account-candidate]: compareAccountCandidate sorts by priority descending, then account ID descending: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1152) +[^candidate-extend]: Selected candidates are extended into incomplete paths: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L1057-L1059), [`MPTIssue.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -1124,7 +1124,7 @@ OrderBookDB maintains four separate indexes for both offer-based order books and ### 4.5.1. OrderBookDB Construction -On startup, `OrderBookDB.update()` ([OrderBookDB.cpp:77-156](https://github.com/XRPLF/rippled/blob/develop/src/xrpld/app/ledger/OrderBookDB.cpp#L77)) scans the entire ledger looking for two types of entries: +On startup, `OrderBookDB.update()` ([OrderBookDBImpl.cpp:93-221](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/app/ledger/OrderBookDBImpl.cpp#L93-L221)) scans the entire ledger looking for two types of entries: **1. Order book directories (`ltDIR_NODE` with `sfExchangeRate`):** @@ -1172,9 +1172,9 @@ The AssetCache provides two query methods: - 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/AssetCache.cpp#L22-L99) -[^get-trust-line-items-filter]: getTrustLineItems filters by direction, excluding trust lines with NoRipple when incoming: [`TrustLine.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/TrustLine.cpp#L57-L58) -[^asset-cache-rebuild]: AssetCache discards incoming subset when outgoing is requested: [`AssetCache.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/AssetCache.cpp#L54-L65) +[^get-ripple-lines-impl]: AssetCache::getRippleLines: [`AssetCache.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/AssetCache.cpp#L66-L76) # 5. Path Ranking @@ -1204,8 +1204,8 @@ To measure this, `computePathRanks` calls `RippleCalc.rippleCalculate()` with an 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L433) -[^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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/paths/Pathfinder.cpp#L425) +[^default-path-partial]: Default path tested with partial payment enabled: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/xrpld/rpc/detail/Pathfinder.cpp#L454) (`convertAmount` defined in [`PathfinderUtils.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -1373,7 +1373,7 @@ The [**Flow engine**](../flow/README.md) takes paths and converts them into exec 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/gregtatcam/rippled/blob/develop/src/xrpld/app/paths/Pathfinder.cpp#L363) +[^getpathliq-no-default]: Default paths disabled in getPathLiquidity: [`Pathfinder.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. @@ -1407,7 +1407,7 @@ def getPathLiquidity(path, minDstAmount): if not convert_all_: # Test remaining liquidity - inputs2 = Input(partialPaymentAllowed=True) + inputs2 = Input(defaultPathsAllowed=False, partialPaymentAllowed=True) rc = RippleCalc.rippleCalculate( view=sandbox, maxAmountIn=srcAmount, @@ -1589,7 +1589,7 @@ This is critical for subscriptions because: 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 asset: +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 @@ -1621,17 +1621,17 @@ The client provides an array of currency/issuer pairs (up to 18 currencies): {"currency": "USD", "issuer": "rIssuer1..."}, {"currency": "EUR", "issuer": "rIssuer2..."}, {"currency": "XRP"}, - {"mpt_id": "00000001B2..."} + {"mpt_issuance_id": "00000001B2..."} ] } ``` For each source asset: -1. PathRequest creates a separate Pathfinder instance -2. The Pathfinder finds paths from that specific source asset to the destination -3. Results are collected in a hash map: `mContext[issue] = pathSet` +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 will always create a PathFinder for any issuer of an IOU even if IOU currency is specified. +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:** diff --git a/docs/payments/README.md b/docs/payments/README.md index a03fba6..f9a63f0 100644 --- a/docs/payments/README.md +++ b/docs/payments/README.md @@ -30,10 +30,10 @@ 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 are using the cross-currency payment execution. Please see [MPT Payment Execution](../mpts/README.md#4-mpt-payment-execution) for a description of MPT transfer mechanics. Before it was moved to cross-currency payment system, Payment transactor completed the steps as outlined in that document manually. +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 only from the open order book (open offers and hybrid offers). See [Domain and Hybrid Offers](../offers/README.md#15-permissioned-dex) and [PermissionedDomains documentation](../permissioned_domains/README.md) for details. +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. @@ -75,7 +75,7 @@ Key flags relevant to payments: - `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 payment to allow another free regular key change + when the account receives a direct XRP payment to allow another free regular key change ### 2.1.3. Reserves @@ -105,7 +105,7 @@ See [AMM Documentation](../amms/README.md#2-ledger-entries) for complete details ## 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). +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 @@ -138,37 +138,42 @@ The `sign` and `submit` RPC commands accept a `build_path` parameter that trigge 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 (this is hardcoded in `TransactionSign.cpp`) -- Only works if `Paths` is not already specified -- Cannot be used for XRP-to-XRP payments +- 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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L53-L62), [`getFlagsMask`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L66-L77), [`preflight`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L81-L245) +[^static-validation]: Static validation (preflight): [`checkExtraFeatures`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L86-L93), [`getFlagsMask`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L97-L109), [`preflight`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L113-L272) -Assuming [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) and [MPTokensV2](https://xrpl.org/resources/known-amendments#mptokensv2) is enabled: +The following preflight failure conditions apply. Cases that depend on a specific amendment are noted inline: - `temDISABLED`: - - transaction contains `sfCredentialIDs` and [Credentials](https://xrpl.org/resources/known-amendments#credentials) amendment is not enabled. - - transaction contains `sfDomainID` and [PermissionedDEX](https://xrpl.org/resources/known-amendments#permissioneddex) is not enabled. + - 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. - `temINVALID_FLAG`: transaction flags contain invalid flags for the payment type. - `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 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` amount 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` currency is different from `Amount` currency. + - `DeliverMin` asset (currency and issuer, or MPT issuance) differs from the `Amount` asset. - `DeliverMin` is greater than `Amount`. -- `temBAD_CURRENCY`: either `Amount` or `SendMax` (or the implied source amount) is a non-native asset that uses the XRP currency code. + - 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 same currency and no `Paths` field. `Paths` are required because perhaps they will allow arbitrage. +- `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. @@ -177,15 +182,17 @@ Assuming [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) an **Validation against the ledger view**[^preclaim-validation] -[^preclaim-validation]: Validation against ledger view (preclaim): [`checkPermission`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L249-L285), [`preclaim`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L288-L385) -[^isLegalNet-sendmax]: Both Amount and SendMax checked via isLegalNet: [`Payment.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L126) -[^delivermin-checks]: DeliverMin checked for legal amount and positive value: [`Payment.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L216-L238) +[^preclaim-validation]: Validation against ledger view (preclaim): [`checkPermission`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L276-L312), [`preclaim`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L315-L402) +[^isLegalNet-sendmax]: Both Amount and SendMax checked via isLegalNet: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L247-L266) +[^domainid-zero]: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L128-L132) +[^preflight-universal]: [`Transactor.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. User cannot fund a new account with a partial payment. - `tecNO_DST_INSUF_XRP`: XRP amount is below reserve. -- `tecDST_TAG_NEEDED` : destination account has `lsfRequireDestTag` flag set and transaction did not specify `DestinationTag` field. +- `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. @@ -202,20 +209,14 @@ Assuming [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) an **Validation during doApply** -**Common validations (all payment types):** - -- `tecNO_PERMISSION`: For cross-currency payments, if DepositPreauth amendment is not enabled and destination has `lsfDepositAuth` flag - **Direct XRP Payments:**[^direct-xrp-payment] -[^direct-xrp-payment]: Direct XRP payment execution: [`Payment.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/Payment.cpp#L607-L698) +[^direct-xrp-payment]: Direct XRP payment execution: [`Payment.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/payment/Payment.cpp#L594-L679) - `tefINTERNAL`: Source account does not exist. -- `tecUNFUNDED_PAYMENT`: Source account cannot send the payment because doing it would leave it with insufficient funds - to cover reserve and pay the fee. +- `tecUNFUNDED_PAYMENT`: sending the payment would leave the source account below its required reserve. When the source account is the fee payer, it must also be able to cover the fee, which may be drawn from the reserve; in a delegated payment the delegate pays the fee, so it is not charged against the source. - `tecNO_PERMISSION`: Destination is a pseudo-account. -- If [DepositAuth](https://xrpl.org/resources/known-amendments#depositauth) amendment is enabled and destination has - `lsfDepositAuth` flag: +- 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) @@ -225,8 +226,7 @@ Assuming [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) an **Cross-Currency Payments:** -- If DepositPreauth amendment is enabled and destination has `lsfDepositAuth` flag, deposit preauthorization is verified - and may fail with: +- 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. @@ -246,7 +246,7 @@ Assuming [MPTokensV1](https://xrpl.org/resources/known-amendments#mptokensv1) an - Fields set: - `Account`: Destination account ID - `Balance`: Payment amount - - `Sequence`: Current ledger sequence if DeletableAccounts amendment is enabled, otherwise 1 + - `Sequence`: the sequence of the ledger in which the account is created **Cross-Currency Payments:** @@ -256,7 +256,6 @@ See [Flow documentation](../flow/README.md) for detailed mechanics of path execu - `AccountRoot` objects are **modified**: - Source account: Balance decreased by actual amount consumed - Destination account: Balance increased by delivered amount - - Destination account (if has `lsfPasswordSpent` flag): Clear the flag - Intermediate accounts in payment path: Balances adjusted according to path execution - `RippleState` objects are **modified**: @@ -291,10 +290,11 @@ See [Flow documentation](../flow/README.md) for detailed mechanics of path execu - `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 transfer fee amount (if transfer fee applies; unchanged if no fee or fee is waived, e.g., clawback) + - 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 @@ -302,8 +302,8 @@ See [Flow documentation](../flow/README.md) for detailed mechanics of path execu 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 both `Amount` and `SendMax` (if present) are XRP 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 a non-XRP asset. +- **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. @@ -314,13 +314,13 @@ Direct XRP payments are the simplest payment type, transferring XRP directly fro **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 (if [DeletableAccounts](https://xrpl.org/resources/known-amendments#deletableaccounts) is enabled) or 1 otherwise. The payment must meet the base reserve requirement (see [Reserves](#213-reserves)), or it fails with `tecNO_DST_INSUF_XRP`. +**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`. -All validation checks-including reserve requirements, deposit authorization, and destination tags-are performed before execution. See [Failure Conditions](#311-failure-conditions) for complete validation rules. +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 involves non-XRP currencies, specifies `SendMax`, or includes `Paths`. +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. diff --git a/docs/permissioned_domains/README.md b/docs/permissioned_domains/README.md index 666533a..a553aa5 100644 --- a/docs/permissioned_domains/README.md +++ b/docs/permissioned_domains/README.md @@ -26,9 +26,9 @@ # 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. +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). Any trading pair can be restricted to a permissioned domain. +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] 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"}]` @@ -39,6 +39,8 @@ For example, a securities exchange creates a PermissionedDomain requiring "accre 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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/paths/BookStep.cpp#L820-L822) + ## 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. @@ -63,14 +65,16 @@ The domain owner always has access to their own domain. All other participants m **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 transaction sequence, making it immutable and globally unique. +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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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: | Domain creation sequence (from transaction) | +| `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 | @@ -114,6 +118,10 @@ When present on an Offer ledger entry, this field indicates the offer exists in - `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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L944-L953) + # 3. Transactions ## 3.1. PermissionedDomainSet Transaction @@ -160,7 +168,7 @@ Creates a new PermissionedDomain (when DomainID is omitted) or updates an existi **If DomainID is omitted (creation)**: - `PermissionedDomain` object is **created** with: - `Owner`: set to Account - - `Sequence`: set to transaction sequence + - `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 @@ -237,7 +245,9 @@ Function: accountInDomain(view, account, domainID) 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. +**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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/CredentialHelpers.cpp#L62-L65), [`verifyValidDomain`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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 fedf784..fb69961 100644 --- a/docs/transactions/README.md +++ b/docs/transactions/README.md @@ -318,7 +318,7 @@ Preflight validation is orchestrated by `Transactor::invokePreflight()` which 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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/applySteps.cpp#L375-L376) +[^preflight-check]: Preflight result check before preclaim: [`applySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/applySteps.cpp#L406-L407) **Transaction Consequences**: @@ -366,8 +366,8 @@ All checks before and including signature verification must return NotTEC codes. 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).[^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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/applySteps.h#L219) -[^doapply-check]: doApply checks likelyToClaimFee flag: [`applySteps.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/applySteps.cpp#L409-L410) +[^likely-to-claim-fee]: likelyToClaimFee flag calculation: [`applySteps.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/tx/applySteps.h#L216) +[^doapply-check]: doApply checks likelyToClaimFee flag: [`applySteps.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/applySteps.cpp#L440-L441) ## 3.3. DoApply @@ -424,7 +424,7 @@ Transaction result codes (TER) are categorized by prefix and meaning: | **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/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/protocol/TER.h#L136-L147) +[^tef]: tef characterization from source comments: [`TER.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/protocol/TER.h#L143-L154) # 5. Ledger Views and Sandboxes @@ -572,6 +572,6 @@ else sbCancel.apply(ctx_.rawView()); ``` -[^conditional-atomicity]: Conditional atomicity pattern in CreateOffer: [`CreateOffer.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/CreateOffer.cpp#L962-L979) +[^conditional-atomicity]: Conditional atomicity pattern in CreateOffer: [`CreateOffer.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/dex/OfferCreate.cpp#L969-L990) -[^balanceHook]: Balance hook description from source comments: [`ReadView.h`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/include/xrpl/ledger/ReadView.h#L153-L157) +[^balanceHook]: Balance hook description from source comments: [`ReadView.h`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/include/xrpl/ledger/ReadView.h#L149-L153) diff --git a/docs/trust_lines/README.md b/docs/trust_lines/README.md index 08592a5..41ed329 100644 --- a/docs/trust_lines/README.md +++ b/docs/trust_lines/README.md @@ -15,9 +15,9 @@ - [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.2.1. Clawback Transaction](#321-clawback-transaction) - - [3.2.1.1. Failure Conditions](#3211-failure-conditions) - - [3.2.1.2. State Changes](#3212-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 @@ -42,7 +42,7 @@ Payments on the XRP Ledger often need to flow through intermediate accounts to r 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. -An issuer who sets `DefaultRipple` (`lsfDefaultRipple`) on their account will have NoRipple cleared on their side of every new trust line. Since the issuer's side is always clear, the both-sides condition can never be met, and payments can always ripple through the issuer. A regular holder who does not set `DefaultRipple` will have NoRipple set on their side of every new trust line. If the holder has two trust lines for the same currency (e.g., USD.IssuerA and USD.IssuerB), both will have NoRipple set on the holder's side, so the both-sides condition is met and payments cannot ripple through the holder's account. This protects the holder from having their balance used as a pass-through without their consent. +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. @@ -133,7 +133,7 @@ see [RippleState Flags](https://xrpl.org/docs/references/protocol/ledger-data/le 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`): Can only modify existing trust lines, cannot create new ones +- **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 @@ -152,7 +152,7 @@ The `sfLowNode` and `sfHighNode` fields in the `RippleState` entry store the dir ### 2.1.5. Reserves - An account only pays a reserve for a trust line when the account owns more than two items total. +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: @@ -205,7 +205,7 @@ to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types - `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`. +- `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`. @@ -215,16 +215,16 @@ to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types - `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`: [fixTrustLinesToSelf](https://xrpl.org/resources/known-amendments#fixtrustlinestoself) amendment is enabled, or if the trust line does not already exist and the source account and destination account are the same. -- `tecNO_DST`: one of [DisallowIncoming](https://xrpl.org/resources/known-amendments#disallowincoming), [AMM](https://xrpl.org/resources/known-amendments#amm) or [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) amendments are enabled and the issuer account does not exist. -- `tecNO_PERMISSION`: [DisallowIncoming](https://xrpl.org/resources/known-amendments#disallowincoming) is enabled and the destination account has `lsfDisallowIncomingTrustline` 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 was already created for a destination with `lsfDisallowIncomingTrustline` and amendment [fixDisallowIncomingV1](https://xrpl.org/resources/known-amendments#fixdisallowincomingv1) was enabled, do not fail. - 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) and the trust line does not already exist. + - `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` @@ -247,9 +247,8 @@ to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types #### 3.1.1.2. State Changes - `RippleState` object is **deleted** if an existing trust line exists when sending `TrustSet` transaction and: - - If [fixTrustLinesToSelf](https://xrpl.org/resources/known-amendments#fixtrustlinestoself) is **not** enabled and source and destination accounts are the same (legacy cleanup for two historical self-referential trust lines). - If the trust line is in its [default state](#11-default-state) after updating it. - - If the currency code of the IOU is `XRP`. + - 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. @@ -259,29 +258,25 @@ to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types - `RippleState` object is **modified**: - If `QualityIn` field was specified in the transaction: - - If `QualityIn` != `1,000,000,000`, set the appropriate quality field (`sfLowQualityIn` for low source account, - `sfHighQualityIn` for high source account) to the `QualityIn` value - - If `QualityIn` = `1,000,000,000`, clear the appropriate quality field (`sfLowQualityIn` for low source - account, `sfHighQualityIn` for high source account) + - 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 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 `lsfLowAuth` for low source account and `lsfHighAuth` for high - source account. + - 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 account no longer requires reserve because its values in a trust line are now default values: - Clear appropriate `lsfLowReserve` or `lsfHighReserve` flag - - If flags are provided, they will modify the currently stored flags value. + - 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**: @@ -294,16 +289,16 @@ to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types - 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: [`SetTrust.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/xrpld/app/tx/detail/SetTrust.cpp#L655-L661) -[^trustcreate-noripple]: NoRipple initialization in trustCreate: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1492-L1509) -[^trustcreate-noripple-src]: Source account NoRipple from transaction flags: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1492-L1495) -[^trustcreate-noripple-dst]: Destination account NoRipple from lsfDefaultRipple: [`View.cpp`](https://github.com/gregtatcam/rippled/blob/a72c3438eb0591a76ac829305fcbcd0ed3b8c325/src/libxrpl/ledger/View.cpp#L1505-L1509) +[^modify-then-delete]: Default state check and deletion after modification: [`TrustSet.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/tx/transactors/token/TrustSet.cpp#L595-L600) +[^trustcreate-noripple]: NoRipple initialization in trustCreate: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/src/libxrpl/ledger/helpers/RippleStateHelpers.cpp#L264-L267) +[^trustcreate-noripple-dst]: Destination account NoRipple from lsfDefaultRipple: [`RippleStateHelpers.cpp`](https://github.com/XRPLF/rippled/blob/0fffe23abc3a42e7d8016fbbd9a0beed3c40bbc9/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. - - If creating a new page would exceed 262,144 pages, the transaction fails with `tecDIR_FULL`. + - 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**: @@ -312,7 +307,7 @@ to [TrustSet Flags](https://xrpl.org/docs/references/protocol/transactions/types - If account no longer requires reserve: - Decrement `sfOwnerCount` by 1, without going below `0`. -### 3.2.1. Clawback Transaction +### 3.1.2. Clawback Transaction **Design note** - in `rippled`, the same transactor implementation of `Clawback` is used to clawback both [IOUs](../glossary.md#iou) and [MPTs](../glossary.md#mpt). `rippled`'s `Clawback` implementation of @@ -329,7 +324,7 @@ Transaction fields are described in [Clawback Fields](https://xrpl.org/docs/refe - *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.2.1.1. Failure Conditions +#### 3.1.2.1. Failure Conditions Static validation @@ -338,15 +333,14 @@ Static validation - 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. + - 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`: If holder's account is an AMM account, such as a SingleAssetVault or AMM account and if - amendment [SingleAssetVault](https://xrpl.org/resources/known-amendments#singleassetvault) is not enabled. +- `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. @@ -354,20 +348,17 @@ Validation against the ledger view - `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`: holder's balance is `0`. +- `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.2.1.2. State Changes +#### 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 account no longer requires reserve because its values in a trust line are now default values after the - transfer: - - Clear appropriate `lsfLowReserve` or `lsfHighReserve` flag - - Adjust owner count for the account + - 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**: @@ -378,8 +369,4 @@ Validation against the ledger view - If removing the trust line empties a non-root directory page, that page is deleted and the directory chain is repaired. -- `AccountRoot` object is **modified** (when `RippleState` is deleted): - - If `lsfLowReserve` flag was set on the deleted trust line: decrement low account's `sfOwnerCount` by 1 - - If `lsfHighReserve` flag was set on the deleted trust line: decrement high account's `sfOwnerCount` by 1 - - Note: Due to the "first two items are free" policy (see [Reserves](#215-reserves)), an account may not have - a reserve flag set even though the trust line exists, so OwnerCount may not be decremented for both accounts. +- `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.