- 1. Introduction
- 2. Precision and Rounding
- 3. AMM Formulas
This document describes helper functions used throughout the AMM implementation in rippled. These functions are called by the transaction handlers documented in deposit.md and withdraw.md, as well as by the payment path finding code, specifically BookStep which integrates AMM liquidity into the Payment Engine.
The functions fall into two categories: precision and rounding functions that handle decimal precision to prevent value leakage, and AMM formulas that implement the mathematical equations for calculating LP tokens and asset amounts based on the weighted geometric mean market maker model.
Terminology:
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.
Functions for handling precision and rounding with the fixAMMv1_3 amendment. These functions ensure that floating-point calculations do not introduce precision errors that could be exploited or cause inconsistencies.
Calculate LP tokens with proper rounding and precision adjustment.
This function is used throughout the AMM implementation when we need to calculate LP token amounts based on a fraction of the pool. It is called by both deposit and withdrawal operations to ensure that the LP tokens issued or redeemed are calculated with proper rounding to protect the pool from precision exploits.
Before the fixAMMv1_3 amendment, simple multiplication could lead to precision issues where adding small amounts to large balances would lose precision. The directional rounding - down for deposits, up for withdrawals - ensures the pool is always protected: depositors get slightly fewer LP tokens, and withdrawers redeem slightly more LP tokens, both favoring the pool.
There are two overloaded versions:
- Simple version: Takes a direct fraction value1 - used by
equalDepositLimit,equalWithdrawLimit - Callback version: Takes lambda callbacks for delayed evaluation2 - used by
singleDepositEPrice,singleWithdrawEPrice
The callback version exists to avoid redundant calculations when the input depends on complex intermediate calculations. The lambdas delay evaluation so that:
- If
fixAMMv1_3is disabled, onlynoRoundCb()is called - If
fixAMMv1_3is enabled, onlyproductCb()is called This prevents computing expensive formulas twice. For simple proportional operations, the simple version is more straightforward.
The Number class uses thread-local global state for its rounding mode3. 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 Number4, so the division respects the global rounding mode without needing to pass the mode as a parameter.
def getRoundedLPTokens(
rules, # Ledger rules
balance, # LP token balance
frac, # Fraction of the pool being deposited/withdrawan
isDeposit # True for deposits, False for withdrawals
):
# Before fixAMMv1_3: Simple multiplication, no special rounding
if not rules.enabled(fixAMMv1_3):
return toSTAmount(balance.asset, balance * frac)
tokens = multiply(balance, frac, 'downward' if isDeposit else 'upward')
return adjustLPTokens(balance, tokens, isDeposit)def getRoundedLPTokens(
rules, # Ledger rules (to check if fixAMMv1_3 is enabled)
noRoundCb, # Lambda that returns the tokens without fixAMMv1_3 rounding
lptAMMBalance, # Total outstanding LP tokens
productCb, # Lambda that returns the multiplier/fraction
isDeposit # True for deposits, False for withdrawals
):
# Before fixAMMv1_3: Use noRoundCb (no special rounding)
if not rules.enabled(fixAMMv1_3):
return toSTAmount(lptAMMBalance.asset, noRoundCb())
# Rounding mode
rm = 'downward' if isDeposit else 'upward'
if isDeposit:
# For deposits: convert productCb() result with rounding
with NumberRoundModeGuard(rm):
tokens = toSTAmount(lptAMMBalance.asset, productCb(), rm)
else:
# For withdrawals: multiply balance by productCb() result
tokens = multiply(lptAMMBalance, productCb(), 'downward' if isDeposit else 'upward')
# Adjust tokens to account for precision loss
return adjustLPTokens(lptAMMBalance, tokens, isDeposit)Adjust LP tokens to account for precision loss when updating the balance.5
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.
This function preemptively calculates what the balance will actually be after the operation completes, accounting for this precision loss. For deposits, it computes (lptAMMBalance + lpTokens) - lptAMMBalance which simulates adding the tokens to the balance and then calculating what the actual change was. For withdrawals, it computes (lpTokens - lptAMMBalance) + lptAMMBalance. By using the actual representable result, the function ensures that the tokens issued or redeemed exactly match what will be reflected in the balance after the ledger applies the operation.
def adjustLPTokens(lptAMMBalance, lpTokens, isDeposit):
# Force downward rounding to ensure adjusted tokens <= requested tokens
with rounding_mode('downward'):
if isDeposit:
return (lptAMMBalance + lpTokens) - lptAMMBalance
else:
return (lpTokens - lptAMMBalance) + lptAMMBalanceCalculate asset amount with proper rounding and precision adjustment.
This function is similar to getRoundedLPTokens, but operates on pool assets (XRP, USD, etc.) rather than LP tokens. The key difference is that the rounding direction is opposite: for deposits, assets round up to ensure the pool receives enough, while for withdrawals, assets round down to ensure the pool retains enough.
There are two overloaded versions:
- Simple version: Takes a direct fraction value6 - used for proportional deposits/withdrawals
- Callback version: Takes lambda callbacks for delayed evaluation7 - used for complex single-asset calculations
Please refer to getRoundedLPTokens for the reasoning and the explanation behind the callback version.
def getRoundedAsset(
rules, # Ledger rules (to check if fixAMMv1_3 is enabled)
balance, # Current pool balance of the asset
frac, # Fraction of the pool being deposited/withdrawn (Number)
isDeposit # True for deposits, False for withdrawals
):
# Before fixAMMv1_3: Simple multiplication, no special rounding
if not rules.enabled(fixAMMv1_3):
return toSTAmount(balance.asset, balance * frac)
return multiply(balance, frac, "upward" if isDeposit else "downward")def getRoundedAsset(
rules, # Ledger rules (to check if fixAMMv1_3 is enabled)
noRoundCb, # Lambda that returns the amount without fixAMMv1_3 rounding
balance, # Current pool balance of the asset
productCb, # Lambda that returns the multiplier/fraction
isDeposit # True for deposits, False for withdrawals
):
# Before fixAMMv1_3: Use noRoundCb (no special rounding)
if not rules.enabled(fixAMMv1_3):
return toSTAmount(balance.asset, noRoundCb())
# Rounding mode
rm = "upward" if isDeposit else "downward"
if isDeposit:
# For deposits: multiply balance by productCb() result
return multiply(balance, productCb(), rm)
else:
# For withdrawals: convert productCb() result directly with rounding
with NumberRoundModeGuard(rm):
return toSTAmount(balance.asset, productCb(), rm)Adjust LP tokens for deposits (outgoing from AMM).8
This wrapper function applies precision adjustments specifically for deposit operations. It calls adjustLPTokens with isDeposit=True, ensuring downward rounding that slightly reduces the LP tokens issued to depositors.
def adjustLPTokensOut(rules, lptAMMBalance, lpTokensDeposit):
if not rules.enabled(fixAMMv1_3):
return lpTokensDeposit
# Deposit uses IsDeposit=True, which rounds tokens DOWNWARD
return adjustLPTokens(lptAMMBalance, lpTokensDeposit, IsDeposit=True)Adjust LP tokens for withdrawals (incoming to AMM).9
This wrapper function applies precision adjustments specifically for withdrawal operations. It calls adjustLPTokens with isDeposit=False.
def adjustLPTokensIn(rules, lptAMMBalance, lpTokensWithdraw, withdrawAll):
if not rules.enabled(fixAMMv1_3) or withdrawAll == WithdrawAll.Yes:
return lpTokensWithdraw
# Withdrawal uses IsDeposit=False, which rounds tokens UPWARD
return adjustLPTokens(lptAMMBalance, lpTokensWithdraw, IsDeposit=False)Adjust asset deposit amount and LP tokens to handle rounding edge cases.10
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 (Equation 3), then adjusts those tokens with adjustLPTokens. However, when we reverse the calculation using ammAssetIn (Equation 4) to verify the deposit amount, rounding can cause the calculated amount to exceed the user's original deposit amount.
If this happens, the function reduces the deposit amount by the difference and recalculates the LP tokens with the adjusted deposit amount. This ensures the final deposit amount never exceeds what the user specified, protecting them from overpaying due to rounding artifacts.
def adjustAssetInByTokens(
rules, # Ledger rules
balance, # Current pool balance of the asset
amount, # User's specified deposit amount
lptAMMBalance, # Total outstanding LP tokens
tokens, # Adjusted LP tokens (from adjustLPTokens)
tfee): # Trading fee
"""
Assumes fixAMMv1_3 is enabled.
"""
# Calculate what deposit amount is needed for the adjusted tokens
assetAdj = ammAssetIn(balance, lptAMMBalance, tokens, tfee)
tokensAdj = tokens
# Check if reverse calculation exceeds user's amount due to rounding
if assetAdj > amount:
# Reduce deposit amount by the difference
adjAmount = amount - (assetAdj - amount)
# Recalculate LP tokens with the reduced amount
t = lpTokensOut(balance, adjAmount, lptAMMBalance, tfee)
tokensAdj = adjustLPTokens(lptAMMBalance, t, isDeposit=True)
# Recalculate asset amount with the new tokens
assetAdj = ammAssetIn(balance, lptAMMBalance, tokensAdj, tfee)
# Return adjusted tokens and the minimum of original or calculated amount
return (tokensAdj, min(amount, assetAdj))Core AMM mathematical formulas for calculating LP tokens and asset amounts.
Swap formulas are used when trading assets through the AMM pool.
Calculate output amount given a specific input amount.11 It answers the question: "If I provide X amount of input asset, how much output asset do I receive?"
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 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.
def swapAssetIn(
pool, # TAmounts<TIn, TOut> with pool.in and pool.out balances
assetIn, # Amount of input asset to swap in
tfee): # Trading fee in 1/10 basis point units
fee = tfee / 100000
with rounding_mode('upward'):
numerator = pool.in * pool.out
with rounding_mode('downward'):
denom = pool.in + assetIn * (1 - fee)
if denom <= 0:
return 0
with rounding_mode('upward'):
ratio = numerator / denom
with rounding_mode('downward'):
swapOut = pool.out - ratio
if swapOut < 0:
return 0
with rounding_mode('downward'):
return swapOutCalculate required input amount for a specific desired output.12 It answers the question: "If I want to receive Y amount of output asset, how much input asset must I provide?"
This is the inverse of 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 when the payment engine needs to deliver a specific amount.
def swapAssetOut(
pool, # TAmounts<TIn, TOut> with pool.in and pool.out balances
assetOut, # Desired amount of output asset
tfee): # Trading fee in 1/10 basis point units
fee = tfee / 100000
with rounding_mode('upward'):
numerator = pool.in * pool.out
with rounding_mode('downward'):
denom = pool.out - assetOut
if denom <= 0:
return MAX_AMOUNT # Cannot extract more than pool has
with rounding_mode('upward'):
ratio = numerator / denom
numerator2 = ratio - pool.in
with rounding_mode('downward'):
feeMult = 1 - fee
with rounding_mode('upward'):
swapIn = numerator2 / feeMult
if swapIn < 0:
return 0
with rounding_mode('upward'):
return swapInThe quality of a swap is defined as input / output (how much you pay per unit received). Lower quality is better for the trader.
For swapAssetOut, we can derive:
quality = in / out
= [((A*B) / (B-out)-A) / (1-fee)] / out
= [A * out] / [(B-out) * (1-fee) * out]
= A / [(B-out) * (1-fee)]
As out increases, the denominator (B-out) decreases, making quality increase (worse for trader).
Example: Quality degradation in a 1,000 USD / 10,000 EUR pool with 0.3% fee
| Desired Output (EUR) | ~Required Input (USD) | ~Quality |
|---|---|---|
| 100 | 10.13 | 0.101 |
| 500 | 52.79 | 0.106 |
| 1,000 | 111.45 | 0.111 |
| 2,000 | 250.75 | 0.125 |
| 5,000 | 1,003.01 | 0.201 |
In rippled, this can be verified using rippled --unittest=AMMCalc and appropriate logging, e.g.:
rippled --unittest=AMMCalc --unittest-arg "swapout,A(USD(1000),EUR(10000)),EUR(5000),300Calculate an AMM offer size such that after the swap, the pool's reserve ratio equals a target quality (typically the best CLOB offer quality).13
This function is called from AMMLiquidity::getOffer() during single-path pathfinding when integrating AMM and CLOB (Central Limit Order Book) liquidity.14 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.
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.
def changeSpotPriceQuality(
pool, # TAmounts<TIn, TOut> with pool.in and pool.out balances
quality, # Target quality (typically best CLOB offer quality)
tfee, # Trading fee in units of 1/100,000
rules): # Ledger rules (for amendment checks)
"""
This pseudocode assumes fixAMMv1_1 is enabled
"""
if isXRP(getIssue(pool.out)):
# TakerGets is XRP - calculate it first
return getAMMOfferStartWithTakerGets(pool, quality, tfee)
else:
# TakerPays is XRP (or both are IOUs) - calculate takerPays first
return getAMMOfferStartWithTakerPays(pool, quality, tfee)Calculate AMM offer starting with takerGets (output) when takerGets is XRP.15
Two scenarios considered:
The algorithm solves for two different scenarios and selects the smallest takerGets to maximize quality (smaller offer means less slippage):
Scenario A: Spot Price Quality after consumption equals target quality
- Condition:
Qsp = (O - o) / (I + i) = targetQuality - Where: O = pool.out, I = pool.in, o = takerGets, i = takerPays
- Substitute
ifrom swap equation:i = (I * o) / ((O - o) * f)wheref = 1 - tfee/100000 - Results in quadratic:
o^2 + o * (I * (1 - 1/f) / Qt - 2*O) + O^2 - I * O / Qt = 0 - NB: The code uses
targetQuality.rate()which returns1/Qt, so the code divides bytargetQuality.rate()where the formula in therippledcomment multiplies by Qt (comment and XLS-30 use Qt = targetQuality). Here, we adopt that Qt = 1 / targetQuality.
Scenario B: Effective Price Quality equals target quality
- Condition:
Qep = o / i = targetQuality - Substitute swap equation and solve for o:
o = O - I / (Qt * f)
def getAMMOfferStartWithTakerGets(pool, targetQuality, tfee):
if targetQuality == 0:
return None
f = 1 - (tfee / 100000) # Fee multiplier
Qt = 1 / targetQuality
# Scenario A: Solve quadratic where spot price after = target quality
# o^2 + o * (I * (1 - 1/f) / Qt - 2*O) + O^2 - I * O / Qt = 0
a = 1
b = pool.in * (1 - 1/f) / Qt - 2 * pool.out
c = pool.out * pool.out - pool.in * pool.out / Qt
takerGetsA = solveQuadraticEqSmallest(a, b, c)
if takerGetsA is None or takerGetsA <= 0:
return None
# Scenario B: Solve where effective price = target quality
# o = O - I / (Qt * f)
takerGetsB = pool.out - pool.in / (Qt * f)
if takerGetsB <= 0:
return None
# Select smallest to maximize quality
takerGets = min(takerGetsA, takerGetsB)
# Round takerGets downward (minimizes offer, maximizes quality)
# This has most impact when takerGets is XRP
takerGets = toAmount(getIssue(pool.out), takerGets, roundingMode=downward)
# Calculate takerPays using swapAssetOut
takerPays = swapAssetOut(pool, takerGets, tfee)
amounts = Amounts(in=takerPays, out=takerGets)
# Try to reduce offer size to improve quality if needed
if Quality(amounts) < targetQuality:
# Reduce takerGets by 0.9999x and recalculate
reducedTakerGets = takerGets * 0.9999
reducedTakerGets = toAmount(getIssue(pool.out), reducedTakerGets, roundingMode=downward)
reducedTakerPays = swapAssetOut(pool, reducedTakerGets, tfee)
amounts = TAmounts(in=reducedTakerPays, out=reducedTakerGets)
return amounts
def solveQuadraticEqSmallest(a, b, c):
"""
Solve quadratic equation and return smallest positive root.
Uses numerically stable citardauq formula:
https://people.csail.mit.edu/bkph/articles/Quadratics.pdf
"""
discriminant = b * b - 4 * a * c
if discriminant < 0:
return None
# Citardauq formula: choose formula based on sign of b
if b > 0:
return (2 * c) / (-b - sqrt(discriminant))
else:
return (2 * c) / (-b + sqrt(discriminant))Similar to getAMMOfferStartWithTakerGets, but solves for takerPays (i) instead of takerGets (o):
Scenario A: Spot Price Quality after consumption equals target quality
- Equation:
Qsp = (O - o) / (I + i) = targetQuality - Substitute
ofrom swap equation:o = (O * i * f) / (I + i * f)wheref = 1 - tfee/100000 - Results in quadratic:
i^2 * f + i * I * (1 + f) + I^2 - I * O / Qt = 0 - NB: The code uses
targetQuality.rate()which returns1/Qt, so the code multiplies bytargetQuality.rate()where the formula divides by Qt. Therippledcomment uses Qt and we keep it for consistency.
Scenario B: Effective Price Quality equals target quality
- Equation:
Qep = o / i = targetQuality - Substitute swap equation and solve for i:
i = O / Qt - I / f
def getAMMOfferStartWithTakerPays(pool, targetQuality, tfee):
"""
Generate AMM offer starting with takerPays when pool.in is XRP
(or both assets are IOUs).
Solves two scenarios and selects smallest takerPays to maximize quality.
"""
if targetQuality == 0:
return None
f = 1 - (tfee / 100000) # Fee multiplier
Qt = targetQuality
# Scenario A: Solve quadratic where spot price after = target quality
# i^2 * f + i * I * (1 + f) + I^2 - I * O / Qt = 0
a = f
b = pool.in * (1 + f)
c = pool.in * pool.in - pool.in * pool.out / Qt
# Use numerically stable citardauq formula
takerPaysA = solveQuadraticEqSmallest(a, b, c)
if takerPaysA is None or takerPaysA <= 0:
return None
# Scenario B: Solve where effective price = target quality
# i = O / Qt - I / f
takerPaysB = pool.out / Qt - pool.in / f
if takerPaysB <= 0:
return None
# Select smallest to maximize quality
takerPays = min(takerPaysA, takerPaysB)
# Round takerPays downward (minimizes offer, maximizes quality)
# This has most impact when takerPays is XRP
takerPays = toAmount(getIssue(pool.in), takerPays, roundingMode=downward)
# Calculate takerGets using swapAssetIn
takerGets = swapAssetIn(pool, takerPays, tfee)
amounts = TAmounts(in=takerPays, out=takerGets)
# Try to reduce offer size to improve quality if needed
if Quality(amounts) < targetQuality:
# Reduce takerPays by 0.9999x and recalculate
reducedTakerPays = takerPays * 0.9999
reducedTakerPays = toAmount(getIssue(pool.in), reducedTakerPays, roundingMode=downward)
reducedTakerGets = swapAssetIn(pool, reducedTakerPays, tfee)
amounts = TAmounts(in=reducedTakerPays, out=reducedTakerGets)
return amountsCalculate LP tokens to receive for a single-asset deposit.16
t1 = [R - (sqrt(f2^2 + R/f1) - f2)] / [1 + (sqrt(f2^2 + R/f1) - f2)]
Where:
t1= LP token ratio (fraction of total supply:t / T)t=lpTokens(LP tokens being issued to the depositor)T=lptAMMBalance(total outstanding LP tokens in the AMM)R=assetDeposit / assetBalance(deposit ratio)f1=feeMult = 1 - tfee(fee multiplier)f2=feeMultHalf = (1 - tfee/2) / f1(half-fee factor)
The final result is: lpTokens = lptAMMBalance * t1
For example, Alice creates an AMM with 100 USD and 100 EUR (0.3% trading fee).
If now Bob deposits a proportional amount of 50 USD and 50 EUR, the new LP token amount for the pool would be SQRT(150 * 150) = 150, and since he is increasing it by 1/3, Bob would receive 50 LP tokens.
However, if Bob deposited only 100 USD, while AMM instance considers 1 USD = 1 EUR, he would get less:
- Deposit ratio: R = 100/100 = 1.0
- Fee multipliers: f1 = 1 - 0.003 = 0.997, f2 = (1 - 0.0015) / 0.997 ~= 1.001505
- Calculate t1: t1 = (R - sqrt(f2^2 + R/f1) + f2) / (1 + sqrt(f2^2 + R/f1) - f2) ~= 0.413591
- LP tokens = 100 * 0.413591 ~= 41.36 LP tokens
The single-asset deposit of 100 USD yields 41.36 LP tokens, while a proportional deposit of 50 USD + 50 EUR (same total value of 100 units) would yield 50 LP tokens. The single-asset deposit is less capital-efficient because it creates an imbalance in the pool ratio, and the trading fee is applied to account for this inefficiency.
def lpTokensOut(
assetBalance, # Current pool balance of the asset
assetDeposit, # Amount of asset to deposit
lptAMMBalance, # Total outstanding LP tokens
tradingFee): # Trading fee in 1/10 basis point units
f1 = 1 - (tradingFee / 100000)
f2 = (1 - (tradingFee / 200000)) / f1
# Calculate the deposit ratio
R = assetDeposit / assetBalance
# Calculate c using Equation 3:
c = sqrt((f2 * f2) + (R / f1)) - f2
frac = (R - c) / (1 + c)
return multiply(lptAMMBalance, frac, roundingMode="downward")Calculate required asset deposit for desired LP tokens.17
Equation 4 is the inverse of Equation 3 (lpTokensOut). We solve Equation 3 for the asset deposit ratio R = assetDeposit / assetBalance.
Starting from Equation 3:
t1 = [R - (sqrt(f2^2 + R/f1) - f2)] / [1 + (sqrt(f2^2 + R/f1) - f2)]
Where the variables represent:
t1= LP token ratio (what fraction of total supply the new tokens represent)t=lpTokens(the LP tokens being issued to the depositor)T=lptAMMBalance(total outstanding LP tokens in the AMM)R=assetDeposit / assetBalance(deposit ratio)f1=feeMult = 1 - tfee(fee multiplier)f2=feeMultHalf = (1 - tfee/2) / f1(half-fee factor)
For convenience, we define:
t2 = 1 + t1(intermediate value)d = f2 - t1/t2(delta term used in derivation)
Algebraic steps:
-
Multiply both sides by denominator:
t1 * [1 + sqrt(f2^2 + R/f1) - f2] = R - sqrt(f2^2 + R/f1) + f2 -
Expand and collect sqrt terms:
sqrt(f2^2 + R/f1) * (t1 + 1) = R + f2 + t1*f2 - t1 -
Substitute
t2 = 1 + t1:sqrt(f2^2 + R/f1) * t2 = R + t2*f2 - t1 -
Divide by
t2:sqrt(f2^2 + R/f1) = R/t2 + f2 - t1/t2 -
Let
d = f2 - t1/t2:sqrt(f2^2 + R/f1) = R/t2 + d -
Square both sides:
f2^2 + R/f1 = (R/t2)^2 + 2*d*R/t2 + d^2 -
Rearrange to quadratic form:
(R/t2)^2 + R*(2*d/t2 - 1/f1) + (d^2 - f2^2) = 0
This gives us the quadratic coefficients:
a = 1/t2^2b = 2*d/t2 - 1/f1c = d^2 - f2^2
We then solve using the quadratic formula to find R, and multiply by assetBalance to get the actual deposit amount.
def ammAssetIn(
assetBalance, # Current pool balance of the asset (B)
lptAMMBalance, # Total outstanding LP tokens (T)
lpTokensDesired, # LP tokens desired (t)
tradingFee): # Trading fee in 1/10 basis points units
"""
Assumes fixAMMv1_3 is enabled
"""
# Fee multipliers (see derivation above for explanation)
f1 = 1 - (tradingFee / 100000)
f2 = (1 - (tradingFee / 200000)) / f1
# Calculate LP token fraction
# Example: 5,000 / 50,000 = 0.1 (wanting 10% of pool)
t1 = lpTokensDesired / lptAMMBalance
# Intermediate value
t2 = 1 + t1
# Delta term (see derivation step 5)
d = f2 - t1 / t2
# Solve quadratic equation: a*R^2 + b*R + c = 0
# where R = assetDeposit / assetBalance (what we're solving for)
# Quadratic coefficients (see derivation step 7)
a = 1 / (t2 * t2)
b = 2 * d / t2 - 1 / f1
c = d * d - f2 * f2
# Solve using quadratic formula: R = (-b + sqrt(b^2 - 4ac)) / (2a)
discriminant = b * b - 4 * a * c
R = (-b + sqrt(discriminant)) / (2 * a)
# Convert fraction R to actual deposit amount and round upward (protect pool)
return multiply(assetBalance, R, roundingMode=upward)Calculate LP tokens to redeem for a single-asset withdrawal.18
t = T * (c - sqrt(c^2 - 4R)) / 2
Where:
t= LP tokens to redeemT=lptAMMBalance(total outstanding LP tokens in the AMM)R=assetWithdraw / assetBalance(withdrawal ratio)c=R * fee + 2 - feefee=tradingFee / 100000(raw fee value, e.g., 0.003 for 0.3% fee)
def lpTokensIn(
assetBalance, # Current pool balance of the asset (B)
assetWithdraw, # Amount of asset to withdraw (b)
lptAMMBalance, # Total outstanding LP tokens (T)
tradingFee): # Trading fee in units of 1/100,000
# Calculate withdrawal ratio
R = assetWithdraw / assetBalance
# Get raw fee value (not a multiplier)
fee = tradingFee / 100000 # e.g., 0.003 for 0.3% fee
# Calculate intermediate value c
c = R * fee + 2 - fee
# Calculate discriminant
discriminant = c * c - 4 * R
# Calculate LP tokens using the formula
# With fixAMMv1_3: round upward (maximize tokens in, protect pool)
frac = (c - sqrt(discriminant)) / 2
return multiply(lptAMMBalance, frac, roundingMode=upward)Calculate asset withdrawal for redeeming LP tokens.19
Equation 8 is the inverse of Equation 7 (lpTokensIn). We solve Equation 7 for the asset withdrawal amount.
Starting from Equation 7:
t = T * (c - sqrt(c^2 - 4R)) / 2
t1 = (c - sqrt(c^2 - 4R)) / 2
Where:
c = R*fee + 2 - feeR=assetWithdraw / assetBalance(withdrawal ratio)t1=lpTokensRedeem / lptAMMBalance(LP token fraction - what we know)fee=tradingFee / 100000(raw fee value, same as in lpTokensIn)
The derivation solves for the withdrawal ratio R, then multiplies by assetBalance to get the actual asset withdrawal amount.
Algebraic steps:
-
Rearrange to isolate the square root:
c - 2*t1 = sqrt(c^2 - 4R) -
Square both sides:
c^2 - 4*c*t1 + 4*t1^2 = c^2 - 4R -
Simplify (c^2 cancels):
-4*c*t1 + 4*t1^2 = -4R -
Divide by -4:
-c*t1 + t1^2 = -R -
Substitute
c = R*fee + 2 - fee:-(R*fee + 2 - fee)*t1 + t1^2 = -R -
Expand:
-t1*R*fee - 2*t1 + t1*fee + t1^2 = -R -
Solve for R:
R = (t1^2 - t1*(2 - fee)) / (t1*fee - 1)
def ammAssetOut(
assetBalance, # Current pool balance of the asset (B)
lptAMMBalance, # Total outstanding LP tokens (T)
lpTokensRedeem, # LP tokens to redeem (t)
tradingFee): # Trading fee in units of 1/100,000
"""
Assumes fixAMMv1_3 is enabled
"""
# Calculate LP token fraction
t1 = lpTokensRedeem / lptAMMBalance
# Get raw fee value (same as in lpTokensIn)
fee = tradingFee / 100000 # e.g., 0.003 for 0.3% fee
# Calculate R using the derived formula (see derivation step 7 above)
R = (t1 * t1 - t1 * (2 - fee)) / (t1 * fee - 1)
# Convert ratio R to actual withdrawal amount and round downward (protect pool)
return multiply(assetBalance, R, roundingMode=downward)Footnotes
-
Simple version of
getRoundedLPTokens:AMMHelpers.cpp↩ -
Callback version of
getRoundedLPTokens:AMMHelpers.cpp↩ -
STAmounttoNumberconversion operator:STAmount.h↩ -
adjustLPTokens:AMMHelpers.cpp↩ -
Simple version of
getRoundedAsset:AMMHelpers.h↩ -
Callback version of
getRoundedAsset:AMMHelpers.cpp↩ -
adjustLPTokensOut:AMMDeposit.cpp↩ -
adjustLPTokensIn:AMMWithdraw.cpp↩ -
adjustAssetInByTokens:AMMHelpers.cpp↩ -
swapAssetIn:AMMHelpers.h↩ -
swapAssetOut:AMMHelpers.h↩ -
changeSpotPriceQuality:AMMHelpers.h↩ -
AMMLiquidity getOffer usage:
AMMLiquidity.cpp↩ -
getAMMOfferStartWithTakerGets:AMMHelpers.h↩ -
lpTokensOut:AMMHelpers.cpp↩ -
ammAssetIn:AMMHelpers.cpp↩ -
lpTokensIn:AMMHelpers.cpp↩ -
ammAssetOut:AMMHelpers.cpp↩