Skip to content

Latest commit

 

History

History
782 lines (629 loc) · 38.5 KB

File metadata and controls

782 lines (629 loc) · 38.5 KB

Index

1. Introduction

The AMMWithdraw transaction allows liquidity providers to redeem their LP tokens for underlying pool assets. This document provides technical implementation details for the withdrawal logic in rippled.

The AMM documentation describes the high-level business logic of withdrawals, including different withdrawal modes, their purposes, and user-facing behavior. This document focuses on the implementation: how the code validates constraints, calculates withdrawal amounts, and executes asset transfers.

Terminology Notes:

Throughout the document we refer to equations by their number in the XLS-30 specification. AMM helpers use token as a subject in many function names. This refers to any supported currency in the system, not only a particular token implementation, like trust lines or MPTs.

2. applyGuts

The applyGuts function1 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). 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 that maintain proportional withdrawals, and three single-asset modes that perform single-sided withdrawals. Each mode handler calculates the withdrawal amounts and LP tokens to burn, then calls the 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.

2.1. applyGuts Pseudo-Code

def applyGuts(sb: &Sandbox, tx: Transaction):
    amount = tx[~sfAmount]
    amount2 = tx[~sfAmount2]
    ePrice = tx[~sfEPrice]
    ammSle = sb.getAMM(tx[sfAsset], tx[sfAsset2])
    if not ammSle:
        return (tecINTERNAL, false)

    ammAccountID = ammSle[sfAccount]

    # Get withdrawer's LP token balance
    lpTokens = ammLPHolds(view, ammSle, account)

    # Determine LP tokens to withdraw
    # For tfWithdrawAll and tfOneAssetWithdrawAll: use all LP tokens
    # Otherwise: use tx[sfLPTokenIn]
    lpTokensWithdraw = tokensWithdraw(lpTokens, tx[~sfLPTokenIn], tx.getFlags())

    # Adjust LP token balance for precision (with fixAMMv1_1)
    # This handles rounding issues for the last LP
    if rules.enabled(fixAMMv1_1):
        verifyAndAdjustLPTokenBalance(sb, lpTokens, ammSle, account)

    # Get current trading fee (with potential discount)
    tfee = getTradingFee(view, ammSle, account)

    # 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)
    if not currentBalances:
        return (currentBalances.error(), false)

    amountBalance, amount2Balance, lptAMMBalance = currentBalances

    subTxType = tx.getFlags() & tfWithdrawSubTx

    # Dispatch to appropriate withdrawal mode handler
    # Returns (result_code, new_lp_token_balance)
    if subTxType & tfTwoAsset:
        # Proportional withdrawal with max constraints on both assets
        result, newLPTokenBalance = equalWithdrawLimit(
            sb,
            ammSle,
            ammAccountID,
            amountBalance,
            amount2Balance,
            lptAMMBalance,
            amount,              # max amount1 to withdraw
            amount2,             # max amount2 to withdraw
            tfee
        )

    elif subTxType & (tfOneAssetLPToken | tfOneAssetWithdrawAll):
        # Single asset withdrawal for specified LP tokens
        result, newLPTokenBalance = singleWithdrawTokens(
            sb,
            ammSle,
            ammAccountID,
            amountBalance,
            lptAMMBalance,
            amount,              # min amount or asset specifier
            lpTokensWithdraw,    # LP tokens to redeem
            tfee
        )

    elif subTxType & tfLimitLPToken:
        # Single asset withdrawal with effective price constraint
        result, newLPTokenBalance = singleWithdrawEPrice(
            sb,
            ammSle,
            ammAccountID,
            amountBalance,
            lptAMMBalance,
            amount,              # min amount
            ePrice,              # min effective price
            tfee
        )

    elif subTxType & tfSingleAsset:
        # Single asset withdrawal for specified amount
        result, newLPTokenBalance = singleWithdraw(
            sb,
            ammSle,
            ammAccountID,
            amountBalance,
            lptAMMBalance,
            amount,              # amount to withdraw
            tfee
        )

    elif subTxType & (tfLPToken | tfWithdrawAll):
        # Proportional withdrawal for LP tokens
        result, newLPTokenBalance = equalWithdrawTokens(
            sb,
            ammSle,
            ammAccountID,
            amountBalance,
            amount2Balance,
            lptAMMBalance,
            lpTokens,
            lpTokensWithdraw,    # LP tokens to redeem
            tfee
        )

    else:
        # Should not happen (validated in preflight)
        return (tecINTERNAL, false)

    if result != tesSUCCESS:
        return (result, false)

    # Delete AMM if empty, or update LP token balance
    res = deleteAMMAccountIfEmpty(
        sb,
        ammSle,
        newLPTokenBalance,
        tx[sfAsset],
        tx[sfAsset2],
        journal
    )

    if not res.second:
        return (res.first, false)

    return (tesSUCCESS, true)

3. getTradingFee

Determines the trading fee for the withdrawer, accounting for auction slot discounts. Same implementation as AMMDeposit, see deposit.md.

4. Multi-Asset Withdrawal Modes

Multi-asset withdrawal modes maintain proportional withdrawals by removing both pool assets simultaneously. This preserves the pool's price (asset ratio) while decreasing liquidity. Since these withdrawals maintain proportional ratios, they incur no trading fees.

The modes are:

  • equalWithdrawTokens (tfLPToken, tfWithdrawAll) - Redeem exact LP tokens or all LP tokens, receive proportional amounts of both assets
  • equalWithdrawLimit (tfTwoAsset) - Specify maximum amounts for both assets, system calculates LP tokens to burn

4.1. equalWithdrawTokens (tfLPToken, tfWithdrawAll)

"I want to redeem exactly X LP tokens, how much of both assets do I get?" (tfLPToken) "Redeem all my LP tokens for both assets." (tfWithdrawAll)

Proportional withdrawal of pool assets for the amount of LP tokens.2

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 to ensure the pool retains sufficient assets.

Example:

A pool has 150 USD, 150 EUR, and 150 LP tokens outstanding. Bob holds 30 LP tokens (20% of the pool).

Case 1: Bob redeems exactly 15 LP tokens (tfLPToken)

  • LP tokens to redeem: 15
  • Fraction of pool: 15 / 150 = 0.1 (10%)
  • USD withdrawn: 150 * 0.1 = 15 USD
  • EUR withdrawn: 150 * 0.1 = 15 EUR
  • Result: Bob redeems 15 LP tokens, receives 15 USD + 15 EUR
  • Bob now holds: 15 LP tokens (10% of remaining pool)

Case 2: Bob redeems all his LP tokens (tfWithdrawAll)

  • LP tokens to redeem: 30 (all Bob's holdings)
  • Fraction of pool: 30 / 150 = 0.2 (20%)
  • USD withdrawn: 150 * 0.2 = 30 USD
  • EUR withdrawn: 150 * 0.2 = 30 EUR
  • Result: Bob redeems 30 LP tokens, receives 30 USD + 30 EUR
  • Bob now holds: 0 LP tokens

4.1.1 equalWithdrawTokens Pseudo-Code

def equalWithdrawTokens(
        view,                 # Ledger view (sandbox)
        ammSle,               # AMM ledger entry
        account,              # Withdrawer account ID
        ammAccount,           # AMM pseudo-account ID
        amountBalance,        # Current pool balance of asset1
        amount2Balance,       # Current pool balance of asset2
        lptAMMBalance,        # Total outstanding LP tokens
        lpTokens,             # Withdrawer's LP token balance
        lpTokensWithdraw,     # LP tokens to redeem
        tfee,                 # Trading fee (not used for proportional)
        freezeHandling,       # How to handle frozen assets
        withdrawAll,          # Whether this is tfWithdrawAll
        priorBalance,         # Withdrawer's prior XRP balance
        journal):             # Debug journal
    # CASE 1: Withdrawing all LP tokens in the pool
    if lpTokensWithdraw == lptAMMBalance:
        # Withdraw all assets, empty the pool
        return withdraw(
            view,
            ammSle,
            ammAccount,
            account,
            amountBalance,
            amountBalance,       # withdraw all of asset1
            amount2Balance,      # withdraw all of asset2
            lptAMMBalance,
            lpTokensWithdraw,
            tfee,
            freezeHandling,
            WithdrawAll=True,    # special handling for complete withdrawal
            priorBalance,
            journal
        )

    # CASE 2: Partial withdrawal
    # Adjust LP tokens for precision (with fixAMMv1_3)
    tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, withdrawAll) # helpers.md#25-adjustlptokensin-withdrawals

    if rules.enabled(fixAMMv1_3) and tokensAdj == 0:
        return (tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, None)

    # Calculate the fraction of the pool being withdrawn
    # Example: 5,000 LP tokens / 50,000 total = 0.1 (10% of pool)
    frac = tokensAdj / lptAMMBalance

    # Calculate withdrawal amounts for both assets
    # With fixAMMv1_3: Round DOWN (conservative, ensures pool keeps enough)
    amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code
    amount2Withdraw = getRoundedAsset(rules, amount2Balance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code

    # Prevent one-sided pool withdrawal due to rounding
    # If either amount rounds to zero, fail so user withdraws more tokens
    if amountWithdraw == 0 or amount2Withdraw == 0:
        return (tecAMM_FAILED, STAmount{}, STAmount{}, STAmount{})

    return withdraw(
        view,
        ammSle,
        ammAccount,
        account,
        amountBalance,
        amountWithdraw,
        amount2Withdraw,
        lptAMMBalance,
        tokensAdj,
        tfee,
        freezeHandling,
        withdrawAll,
        priorBalance,
        journal
    )

4.2. equalWithdrawLimit (tfTwoAsset)

"I want to withdraw up to X and Y, how many LP tokens do I burn?"

The user specifies maximum amounts they want to withdraw for both assets (Amount and Amount2).3 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.

The function tries two strategies to maximize the withdrawal within the user's constraints. First, it attempts to withdraw all of Amount by calculating the pool fraction this represents (frac = Amount / amountBalance), converting this to LP tokens with proper rounding, then recalculating the fraction from the rounded LP tokens (frac = adjustFracByTokens(...)) to ensure precision consistency. Using this adjusted fraction, it calculates the proportional amount2 needed. If this amount2 fits within Amount2, the withdrawal proceeds immediately.

Only if the first strategy fails does it try the second strategy: starting with all of Amount2, going through the same fraction -> LP tokens -> adjusted fraction → amount1 calculation. If the calculated amount1 exceeds Amount, the entire withdrawal fails.

Example:

There is an AMM with 100 USD and 100 EUR (1:1 ratio), 100 LP tokens outstanding. Bob holds 50 LP tokens and wants to make a proportional withdrawal.

Case 1: Bob tries to withdraw up to 30 USD + 20 EUR

  • Strategy 1: Use all 30 USD and needs 30 EUR to maintain 1:1 ratio - FAILS (only wants 20 EUR)
  • Strategy 2: Use all 20 EUR and needs 20 USD to maintain 1:1 ratio - SUCCESS (wants up to 30 USD)
  • Result: Withdraws 20 USD + 20 EUR, redeems 20 LP tokens

Case 2: Bob tries to withdraw up to 20 USD + 30 EUR

  • Strategy 1: Use all 20 USD and needs 20 EUR to maintain 1:1 ratio - SUCCESS (wants up to 30 EUR)
  • Result: Withdraws 20 USD + 20 EUR, redeems 20 LP tokens

4.2.1 equalWithdrawLimit Pseudo-Code

def equalWithdrawLimit(
        view,                 # Ledger view (sandbox)
        ammSle,               # AMM ledger entry
        ammAccount,           # AMM pseudo-account ID
        amountBalance,        # Current pool balance of asset1
        amount2Balance,       # Current pool balance of asset2
        lptAMMBalance,        # Total outstanding LP tokens
        amount,               # User's max amount1 to withdraw
        amount2,              # User's max amount2 to withdraw
        tfee):                # Trading fee (not used for proportional)
    # STRATEGY 1: Try using all of amount (asset1)
    # Calculate what fraction of the pool this represents
    frac = amount / amountBalance

    # Calculate LP tokens for this fraction
    # Using simple version of getRoundedLPTokens (direct fraction)
    tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit=False) # helpers.md#211-getroundedlptokens-simple-pseudo-code

    if rules.enabled(fixAMMv1_3) and tokensAdj == 0:
        return (tecAMM_INVALID_TOKENS, STAmount{})

    # Adjust fraction based on rounded tokens (for precision consistency)
    frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac)

    # Calculate how much asset2 would be withdrawn for this fraction
    # Using simple version of getRoundedAsset (direct fraction)
    amount2Withdraw = getRoundedAsset(rules, amount2Balance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code

    # Check if calculated amount2 fits within user's max constraint
    if amount2Withdraw <= amount2:
        # Success! Use all of amount, calculated amount2Withdraw
        return withdraw(
            view,
            ammSle,
            ammAccount,
            amountBalance,
            amount,              # withdraw all of asset1
            amount2Withdraw,     # calculated asset2
            lptAMMBalance,
            tokensAdj,
            tfee
        )

    # STRATEGY 2: Strategy 1 failed, try using all of amount2
    # Calculate what fraction of the pool amount2 represents
    frac = amount2 / amount2Balance

    # Calculate how much asset1 would be withdrawn for this fraction (preliminary)
    # Using simple version of getRoundedAsset (direct fraction)
    amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code

    # Calculate LP tokens for this fraction
    # Using simple version of getRoundedLPTokens (direct fraction)
    tokensAdj = getRoundedLPTokens(rules, lptAMMBalance, frac, IsDeposit=False) # helpers.md#211-getroundedlptokens-simple-pseudo-code

    if rules.enabled(fixAMMv1_3) and tokensAdj == 0:
        return (tecAMM_INVALID_TOKENS, STAmount{})

    # Adjust fraction based on rounded tokens (for precision consistency)
    frac = adjustFracByTokens(rules, lptAMMBalance, tokensAdj, frac)

    # Recalculate asset1 amount with adjusted fraction
    # Using simple version of getRoundedAsset (direct fraction)
    amountWithdraw = getRoundedAsset(rules, amountBalance, frac, IsDeposit=False) # helpers.md#231-getroundedasset-simple-pseudo-code

    # Check if calculated amount fits within user's max constraint
    if rules.enabled(fixAMMv1_3):
        if amountWithdraw > amount:
            return (tecAMM_FAILED, STAmount{})

    # Success! Use calculated amountWithdraw, all of amount2
    return withdraw(
        view,
        ammSle,
        ammAccount,
        amountBalance,
        amountWithdraw,      # calculated asset1
        amount2,             # withdraw all of asset2
        lptAMMBalance,
        tokensAdj,
        tfee
    )

5. Single-Asset Withdrawal Modes

Single-asset withdrawal modes allow users to withdraw only one asset instead of both assets proportionally. Unlike multi-asset withdrawals that maintain the pool ratio, single-asset withdrawals change the pool composition. Because they alter the pool ratio, trading fees apply to single-asset withdrawals. There are three modes:

  • singleWithdraw (tfSingleAsset) - User specifies withdrawal amount, system calculates LP tokens to redeem
  • singleWithdrawTokens (tfOneAssetLPToken, tfOneAssetWithdrawAll) - User specifies exact LP tokens to redeem or all LP tokens, withdraws calculated amount of single asset
  • singleWithdrawEPrice (tfLimitLPToken) - User specifies minimum effective price limit

5.1. singleWithdraw (tfSingleAsset)

"I want to withdraw X amount, how many LP tokens must I redeem?"

The user specifies Amount (the asset amount to withdraw) and the function calculates how many LP tokens must be redeemed.4 Since this is a single-asset withdrawal that changes the pool ratio, a trading fee applies. The function uses lpTokensIn (Equation 7) to calculate the LP tokens based on the withdrawal amount and trading fee, then adjusts the result for precision with adjustLPTokensIn. 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.

5.1.1. singleWithdraw Pseudo-Code

def singleWithdraw(
        view,              # Ledger view (sandbox)
        ammSle,            # AMM ledger entry
        ammAccount,        # AMM pseudo-account ID
        amountBalance,     # Current pool balance of the asset
        lptAMMBalance,     # Total outstanding LP tokens
        amount,            # Amount to withdraw
        tfee):             # Trading fee (for single-asset withdrawal)
    # Calculate LP tokens using the single-asset withdrawal formula
    # lpTokensIn solves: "How many LP tokens to redeem to get `amount` assets?"
    tokens = lpTokensIn(amountBalance, amount, lptAMMBalance, tfee) # helpers.md#331-lptokensin-equation-7

    # Adjust LP tokens for precision (with fixAMMv1_3)
    tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, tokens, isWithdrawAll(tx)) # helpers.md#25-adjustlptokensin-withdrawals

    if tokensAdj == 0:
        if not rules.enabled(fixAMMv1_3):
            return (tecAMM_FAILED, STAmount{})
        else:
            return (tecAMM_INVALID_TOKENS, STAmount{})

    # Adjust withdrawal amount based on adjusted tokens
    # This ensures the reverse calculation produces consistent results
    tokensAdj, amountWithdrawAdj = adjustAssetOutByTokens(
        rules, amountBalance, amount, lptAMMBalance, tokensAdj, tfee)

    if rules.enabled(fixAMMv1_3) and tokensAdj == 0:
        return (tecAMM_INVALID_TOKENS, STAmount{})

    return withdraw(
        view,
        ammSle,
        ammAccount,
        amountBalance,
        amountWithdrawAdj,   # adjusted: actual amount to withdraw
        None,                # single-asset withdrawal (no asset2)
        lptAMMBalance,
        tokensAdj,           # calculated: LP tokens to redeem
        tfee
    )

5.2. singleWithdrawTokens (tfOneAssetLPToken, tfOneAssetWithdrawAll)

"I'll redeem exactly X LP tokens, how much asset (single asset only) do I get?" (tfOneAssetLPToken) "Redeem all my LP tokens for a single asset." (tfOneAssetWithdrawAll)

Withdraw a single asset by redeeming specified LP tokens.5

This function handles the reverse calculation from singleWithdraw: the user specifies the exact number of LP tokens to redeem (using LPTokenIn for tfOneAssetLPToken, or all LP tokens for tfOneAssetWithdrawAll), and the function calculates the withdrawal amount of a single asset. The user can provide Amount as a minimum constraint on how much they expect to receive.

The function first adjusts the LP tokens for precision using adjustLPTokensIn, then uses ammAssetOut (Equation 8) to calculate the withdrawal amount by solving the inverse single-asset withdrawal problem. If the calculated amount is less than the user's Amount constraint (when non-zero), the transaction fails with tecAMM_FAILED.

5.2.1. singleWithdrawTokens Pseudo-Code

def singleWithdrawTokens(
        view,              # Ledger view (sandbox)
        ammSle,            # AMM ledger entry
        ammAccount,        # AMM pseudo-account ID
        amountBalance,     # Current pool balance of the asset
        lptAMMBalance,     # Total outstanding LP tokens
        amount,            # Min asset to receive (or 0 for no min, or asset specifier)
        lpTokensWithdraw,  # LP tokens to redeem
        tfee):             # Trading fee (for single-asset withdrawal)
    # Adjust LP tokens for precision (with fixAMMv1_3)
    tokensAdj = adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, isWithdrawAll(tx)) # helpers.md#25-adjustlptokensin-withdrawals

    if rules.enabled(fixAMMv1_3) and tokensAdj == 0:
        return (tecAMM_INVALID_TOKENS, STAmount{})

    # Calculate withdrawal amount using ammAssetOut formula
    amountWithdraw = ammAssetOut(amountBalance, lptAMMBalance, tokensAdj, tfee) # helpers.md#332-ammassetout-equation-8

    # Check if calculated amount meets user's minimum (if specified)
    if amount == 0 or amountWithdraw >= amount:
        # Either no minimum specified, or calculated amount meets minimum
        return withdraw(
            view,
            ammSle,
            ammAccount,
            amountBalance,
            amountWithdraw,      # calculated: amount to withdraw
            None,                # single-asset withdrawal
            lptAMMBalance,
            tokensAdj,           # exact: LP tokens to redeem
            tfee
        )

    # Calculated amount is less than user's minimum
    return (tecAMM_FAILED, STAmount{})

5.3. singleWithdrawEPrice (tfLimitLPToken)

"I'll withdraw (single asset), but only if the effective price per LP token is reasonable."

Withdraw a single asset with an effective price constraint.

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.

5.3.1. singleWithdrawEPrice Pseudo-Code

def singleWithdrawEPrice(
        view,              # Ledger view (sandbox)
        ammSle,            # AMM ledger entry
        ammAccount,        # AMM pseudo-account ID
        amountBalance,     # Current pool balance of the asset
        lptAMMBalance,     # Total outstanding LP tokens
        amount,            # Min asset to receive (or 0 for no min)
        ePrice,            # Min effective price (LPTokenIn / AssetOut)
        tfee):             # Trading fee (for single-asset withdrawal)
    # Calculate intermediate value: B * E (balance * effective price)
    ae = amountBalance * ePrice

    # Get fee multiplier
    f = getFee(tfee)  # fee in units of 1/100,000 (e.g., 30 -> 0.0003)

    # Calculate LP tokens using derived formula
    # t = T*(T + B*E*(f-2)) / (T*f - B*E)
    tokNoRoundCb = lambda: (
        lptAMMBalance * (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae)
    )
    tokProdCb = lambda: (
        (lptAMMBalance + ae * (f - 2)) / (lptAMMBalance * f - ae)
    )

    tokensAdj = getRoundedLPTokens(
        rules, tokNoRoundCb, lptAMMBalance, tokProdCb, IsDeposit=False) # helpers.md#212-getroundedlptokens-callback-pseudo-code

    if tokensAdj <= 0:
        if not rules.enabled(fixAMMv1_3):
            return (tecAMM_FAILED, STAmount{})
        else:
            return (tecAMM_INVALID_TOKENS, STAmount{})

    # Calculate withdrawal amount from tokens and effective price
    # amountWithdraw = tokensAdj / ePrice
    amtNoRoundCb = lambda: tokensAdj / ePrice
    amtProdCb = lambda: tokensAdj / ePrice

    amountWithdraw = getRoundedAsset(
        rules, amtNoRoundCb, amount, amtProdCb, IsDeposit=False) # helpers.md#232-getroundedasset-callback-pseudo-code

    # Check if calculated amount meets user's minimum (if specified)
    if amount == 0 or amountWithdraw >= amount:
        return withdraw(
            view,
            ammSle,
            ammAccount,
            amountBalance,
            amountWithdraw,      # calculated: amount to withdraw
            None,                # single-asset withdrawal
            lptAMMBalance,
            tokensAdj,           # calculated: LP tokens to redeem
            tfee
        )

    # Calculated amount is less than user's minimum
    return (tecAMM_FAILED, STAmount{})

6. Common Withdraw Function

The withdraw() function6 serves as the final common pathway for all withdrawal modes, executing the actual asset transfers after mode-specific handlers determine the withdrawal amounts.

This function orchestrates a sequenced validation and execution flow. It begins by verifying the withdrawer holds sufficient LP tokens to redeem, then enforces pool integrity constraints that prevent malformed states.

The function prohibits one-sided pool withdrawals - situations where all of one asset would be withdrawn while the other remains. When a withdrawal would redeem all outstanding LP tokens, the function mandates that all pool assets must also be withdrawn, preventing orphaned assets in an empty pool.

Before executing transfers, the function checks whether the withdrawer has adequate XRP reserves if new trust lines or MPTokens need creation. For MPTs, this includes verifying proper authorization from the issuer. The function then transfers each withdrawn asset from the AMM account to the withdrawer, waiving transfer fees as AMM operations are privileged. Finally, it burns the redeemed LP tokens by calling redeemIOU, which reduces both the withdrawer's LP token balance and the total outstanding token supply.

6.1. withdraw Pseudo-Code

def withdraw(
        view,              # Ledger view (sandbox)
        ammSle,            # AMM ledger entry
        ammAccount,        # AMM pseudo-account ID
        account,           # Withdrawer account ID
        amountBalance,     # Current pool balance of asset1
        amountWithdraw,    # Amount1 to withdraw
        amount2Withdraw,   # Optional: amount2 to withdraw
        lpTokensAMMBalance,  # Total outstanding LP tokens
        lpTokensWithdraw,  # LP tokens to redeem
        tfee,              # Trading fee
        freezeHandling,    # How to handle frozen assets
        withdrawAll,       # Whether this is a complete withdrawal
        priorBalance:      # Withdrawer's prior XRP balance
    # Get withdrawer's current LP token balance
    lpTokens = ammLPHolds(view, ammSle, account, journal)

    # Get current pool balances (accounting for freezes)
    currentBalances = ammHolds(view, ammSle, amountWithdraw.issue, None, freezeHandling)
    if not currentBalances:
        return (currentBalances.error(), STAmount{}, STAmount{}, STAmount{})

    curBalance, curBalance2, _ = currentBalances

    # Adjust amounts for precision (with fixAMMv1_3, this shouldn't be needed as we have already adjusted and rounded all numbers properly)
    # When withdrawing all, skip adjustment and use exact values
    if withdrawAll == No:
        amountWithdrawActual, amount2WithdrawActual, lpTokensWithdrawActual = \
            adjustAmountsByLPTokens(
                amountBalance,
                amountWithdraw,
                amount2Withdraw,
                lpTokensAMMBalance,
                lpTokensWithdraw,
                tfee,
                IsDeposit=False
            )
    else:
        amountWithdrawActual = amountWithdraw
        amount2WithdrawActual = amount2Withdraw
        lpTokensWithdrawActual = lpTokensWithdraw

    # Validate LP tokens
    if lpTokensWithdrawActual <= 0 or lpTokensWithdrawActual > lpTokens:
        return (tecAMM_INVALID_TOKENS, STAmount{}, STAmount{}, STAmount{})

    # With fixAMMv1_1: Additional validation
    if rules.enabled(fixAMMv1_1) and lpTokensWithdrawActual > lpTokensAMMBalance:
        return (tecINTERNAL, STAmount{}, STAmount{}, STAmount{})

    # Prevent one-sided pool withdrawal
    # If withdrawing all of one asset but not the other, fail
    # This ensures pools are always balanced (or completely empty)
    if (amountWithdrawActual == curBalance and amount2WithdrawActual != curBalance2) or \
       (amount2WithdrawActual == curBalance2 and amountWithdrawActual != curBalance):
        return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{})

    # If redeeming all LP tokens, must withdraw all assets
    # This prevents situations where LP tokens are zero but assets remain
    if lpTokensWithdrawActual == lpTokensAMMBalance and \
       (amountWithdrawActual != curBalance or amount2WithdrawActual != curBalance2):
        return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{})

    # Check withdrawal doesn't exceed pool balance
    if amountWithdrawActual > curBalance or amount2WithdrawActual > curBalance2:
        return (tecAMM_BALANCE, STAmount{}, STAmount{}, STAmount{})

    # Helper function to check reserve requirements (with fixAMMv1_2)
    # Checks if withdrawer has sufficient XRP reserve for trust line or MPToken creation
    def sufficientReserve(asset):
        if not rules.enabled(fixAMMv1_2) or isXRP(asset):
            return (tesSUCCESS, None)

        # Check if trust line (for IOUs) or MPToken (for MPTs) exists
        if isIOU(asset):
            assetExists = view.exists(keylet.line(account, asset.issue))
            mptokenKey = None
        else:  # MPT
            issuanceKey = keylet.mptIssuance(asset.mptID)
            mptokenKey = keylet.mptoken(issuanceKey, account)
            assetExists = view.exists(mptokenKey)
            if assetExists:
                mptokenKey = None  # Already exists, no need to create

        if not assetExists:
            sleAccount = view.peek(keylet.account(account))
            if not sleAccount:
                return (tecINTERNAL, None)

            balance = sleAccount[sfBalance].xrp
            ownerCount = sleAccount[sfOwnerCount]

            reserve = view.fees().accountReserve(ownerCount + 1) if ownerCount >= 2 else 0

            # For IOUs: use max of prior and current balance
            # For MPTs: use prior balance only
            balanceToCheck = max(priorBalance, balance) if isIOU(asset) else priorBalance

            if balanceToCheck < reserve:
                return (tecINSUFFICIENT_RESERVE, None)

            # 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
    def createMPToken(asset, mptokenKey):
        if mptokenKey and account != asset.getIssuer():
            # Must authorize MPToken
            if requireAuth(view, asset.mptIssue, account, WeakAuth) != tesSUCCESS:
                return err

            if checkCreateMPT(view, asset.mptIssue, account, journal) != tesSUCCESS:
                return err

        return tesSUCCESS

    # Check reserve and create MPToken for asset1
    result, mptokenKey = sufficientReserve(amountWithdrawActual.asset)
    if result != tesSUCCESS:
        return (result, STAmount{}, STAmount{}, STAmount{})

    result = createMPToken(amountWithdrawActual.asset, mptokenKey)
    if result != tesSUCCESS:
        return (result, STAmount{}, STAmount{}, STAmount{})

    # Transfer asset1 from AMM to withdrawer
    result = accountSend(
        view,
        ammAccount,           # from: AMM pseudo-account
        account,              # to: withdrawer
        amountWithdrawActual, # amount
        WaiveTransferFee=Yes  # AMM withdrawals don't pay transfer fees
    )
    if result != tesSUCCESS:
        return (result, STAmount{}, STAmount{}, STAmount{})

    # If two-asset withdrawal, check reserve, create MPToken, and transfer asset2
    if amount2WithdrawActual:
        # Check reserve and create MPToken for asset2
        result, mptokenKey = sufficientReserve(amount2WithdrawActual.asset)
        if result != tesSUCCESS:
            return (result, STAmount{}, STAmount{}, STAmount{})

        result = createMPToken(amount2WithdrawActual.asset, mptokenKey)
        if result != tesSUCCESS:
            return (result, STAmount{}, STAmount{}, STAmount{})

        result = accountSend(
            view,
            ammAccount,
            account,
            amount2WithdrawActual,
            WaiveTransferFee=Yes
        )
        if result != tesSUCCESS:
            return (result, STAmount{}, STAmount{}, STAmount{})

    # Redeem (burn) LP tokens
    # This decreases the trust line balance and may delete the trust line
    result = redeemIOU(
        view,
        account,
        lpTokensWithdrawActual,
        lpTokensWithdrawActual.issue,
        journal
    )
    if result != tesSUCCESS:
        return (result, STAmount{}, STAmount{}, STAmount{})

    # Return success with new LP token balance and actual withdrawal amounts
    return (
        tesSUCCESS,
        lpTokensAMMBalance - lpTokensWithdrawActual,
        amountWithdrawActual,
        amount2WithdrawActual
    )

Footnotes

  1. AMMWithdraw::applyGuts: AMMWithdraw.cpp

  2. AMMWithdraw::equalWithdrawTokens: AMMWithdraw.cpp

  3. AMMWithdraw::equalWithdrawLimit: AMMWithdraw.cpp

  4. AMMWithdraw::singleWithdraw: AMMWithdraw.cpp

  5. AMMWithdraw::singleWithdrawTokens: AMMWithdraw.cpp

  6. AMMWithdraw::withdraw: AMMWithdraw.cpp