The Automated Market Maker — pool / pseudo-account design, LP tokens, and the constant-product maths.
What you'll learn
≈75 min · Advanced · builds on Transaction ordering
The final phase is about extending the protocol, and we start with one of its richest features, the AMM. In this module you'll learn the ltAMM ledger entry, the pseudo-account that holds a pool with no signing key, LP tokens, and the constant-product math that prices every swap. It's DeFi, built natively into the ledger.
In brief: what an AMM is, and how XRPL's built-in one differs from a standalone AMM.
An Automated Market Maker is a decentralized exchange mechanism that uses a mathematical formula to price assets. Instead of matching buyers and sellers through an order book, AMMs use liquidity pools where:
| Feature | Traditional AMM (Uniswap v2) | XRPL AMM |
|---|---|---|
| Formula | x * y = k | x * y = k (same) |
| Fee Model | Fixed fee | Governance-voted fee |
| Fee Discount | None | Auction slot mechanism |
| Order Book | Separate | Integrated with CLOB |
| LP Tokens | ERC-20 | Native XRPL tokens |
| Pathfinding | External | Native integration |
In brief: the on-ledger object that stores a pool's assets and settings.
Every AMM pool is represented by an ltAMM ledger entry (type 0x0079). This entry stores the pool configuration, current state, and governance data.
Location: include/xrpl/protocol/detail/ledger_entries.macro
LEDGER_ENTRY(ltAMM, 0x0079, AMM, ({
{sfAccount, REQUIRED}, // Pseudo-account ID
{sfTradingFee, DEFAULT}, // Trading fee, units of 1/100,000 (1000 = 1%)
{sfVoteSlots, OPTIONAL}, // Array of up to 8 VoteEntry objects
{sfAuctionSlot, OPTIONAL}, // Auction slot holder and settings
{sfLPTokenBalance, REQUIRED}, // Total outstanding LP tokens
{sfAsset, REQUIRED}, // First asset issue
{sfAsset2, REQUIRED}, // Second asset issue
{sfOwnerNode, REQUIRED}, // Directory linkage
{sfPreviousTxnID, OPTIONAL}, // Last transaction hash
{sfPreviousTxnLgrSeq, OPTIONAL}, // Last transaction ledger sequence
}))
sfAccount - The pseudo-account that holds pool assets. This is a special account created during AMMCreate with no signing keys.
sfTradingFee - Current trading fee in units of 1/100,000 (0.001% per unit, so the maximum 1000 = 1%). Careful: these are not standard basis points (1 bp = 0.01%). Determined by weighted LP voting.
sfLPTokenBalance - Total LP tokens outstanding. Represents total pool ownership claims.
sfAsset / sfAsset2 - The two assets in the pool. Always ordered: Asset < Asset2 (lexicographic ordering ensures uniqueness).
// Get AMM keylet from two issues
Keylet keylet::amm(Issue const& issue1, Issue const& issue2);
// The issues are automatically ordered internally
// keylet::amm(USD/Gateway, XRP) == keylet::amm(XRP, USD/Gateway)
Location: include/xrpl/protocol/Keylet.h
In brief: a keyless account that actually holds the pool's funds.
Each AMM pool operates through a pseudo-account - a special XRPL account with unique properties.
sfAMMID field linking to ltAMM// From AMMCreate.cpp - doApply()
// 1. Generate pseudo-account ID from asset pair
auto const ammAccountID = calcAccountID(
keylet::amm(ctx_.tx[sfAmount], ctx_.tx[sfAmount2]).key);
// 2. Create AccountRoot with disabled master key
auto sleAMMRoot = std::make_shared<SLE>(keylet::account(ammAccountID));
sleAMMRoot->setAccountID(sfAccount, ammAccountID);
sleAMMRoot->setFieldU32(sfSequence, 0);
sleAMMRoot->setFieldAmount(sfBalance, STAmount{});
sleAMMRoot->setFieldU32(sfFlags, lsfDisableMaster); // No signing!
sleAMMRoot->setFieldH256(sfAMMID, keylet::amm(...).key);
// 3. Insert into ledger
ctx_.view().insert(sleAMMRoot);
The pseudo-account design ensures:
Key idea. An AMM pool is owned by a pseudo-account with no usable signing key. No one can move the pool's funds directly; only the AMM transactions can.
In brief: the tokens that represent each provider's share of the pool.
LP (Liquidity Provider) tokens represent fractional ownership of pool assets. XRPL implements them as native tokens with a special currency code.
// LP token currency = 0x03 + SHA512Half(sorted asset currencies)
// Construction:
Currency lptCurrency;
lptCurrency[0] = 0x03; // AMM LP token marker
// Remaining 19 bytes = truncated hash of sorted currencies
auto const hash = sha512Half(
std::min(currency1, currency2),
std::max(currency1, currency2)
);
std::memcpy(&lptCurrency[1], hash.data(), 19);
Location: src/libxrpl/ledger/helpers/AMMHelpers.cpp
| Property | Value |
|---|---|
| Currency Code | 0x03 prefix + 19-byte hash |
| Issuer | AMM pseudo-account |
| Precision | 16 significant digits |
| Transferable | Yes (standard trustlines) |
| Tradeable | Yes (can be traded on DEX) |
When a pool is created, the initial LP token balance is:
LPT_initial = sqrt(Amount1 * Amount2)
This geometric mean ensures the initial LP tokens represent equal value from both assets.
Code Reference: src/libxrpl/ledger/helpers/AMMHelpers.cpp:5-17
STAmount ammLPTokens(
STAmount const& asset1,
STAmount const& asset2,
Issue const& lptIssue)
{
// sqrt(asset1 * asset2)
auto const tokens = root2(asset1 * asset2);
return toSTAmount(lptIssue, tokens);
}
In brief: LPs vote the pool's fee, weighted by their share.
LPs can vote on the trading fee through the sfVoteSlots array. This implements decentralized governance for pool parameters.
Why Governance-Voted Fees?
Unlike traditional AMMs with fixed fees (e.g., Uniswap's 0.3%), XRPL allows LPs to collectively decide the optimal fee for their pool. This enables:
When to Use AMMVote:
// VoteEntry fields
struct VoteEntry {
AccountID Account; // Voter's account
uint16_t FeeVal; // Proposed fee, units of 1/100,000 (0-1000)
uint32_t VoteWeight; // LP token ownership share of the pool
};
// Up to 8 VoteEntry objects per AMM
constexpr std::uint16_t VOTE_MAX_SLOTS = 8;
What happens when Eve (with 2,000 LP tokens) wants to vote?
In brief: rent the zero-fee slot by burning LP tokens.
The auction slot mechanism allows LPs to bid for reduced trading fees during a 24-hour period.
Why Auction Slots?
The auction slot creates a market mechanism where those who benefit most from low fees pay for that privilege. This is particularly valuable for:
When to Use AMMBid:
Economic Design:
// AuctionSlot fields
struct AuctionSlot {
AccountID Account; // Current slot holder
NetClock::time_point Expiration; // Slot expiration (24h from bid)
uint16_t DiscountedFee; // Fee paid by holder (typically 0)
STAmount Price; // Bid amount in LP tokens
std::vector<AccountID> AuthAccounts; // Up to 4 authorized accounts
};
Time Division:
Bidding Formula:
min_bid = current_price * 1.05 + min_slot_price
// Where min_slot_price considers time decay:
// As time passes, minimum bid decreases
Refund Mechanism:
refund_to_previous = (1 - time_fraction_used) * previous_price
burned = new_bid - refund
In brief: two governance gadgets, side by side.
| Aspect | VoteSlots | AuctionSlot |
|---|---|---|
| Purpose | Set trading fee for everyone | Get discounted fee for yourself |
| Slots | Up to 8 | Exactly 1 |
| Cost | Free (just hold LP tokens) | Pay LP tokens to win |
| Duration | Permanent (until vote changes) | 24 hours |
| Benefit | Influence pool economics | Personal trading advantage |
| Who benefits | All pool users | Slot holder + 4 authorized |
| Transaction | AMMVote | AMMBid |
In brief: AMM offers compete with the order book on quality.
AMM pools don't just exist in isolation - they're deeply integrated with XRPL's pathfinding engine (covered in detail in AMM Pathfinding).
In brief: the feature flags that switched all of this on.
The AMM feature is controlled by the featureAMM amendment.
Location: include/xrpl/protocol/detail/features.macro
XRPL_FEATURE(AMM, Supported::yes, VoteBehavior::DefaultNo)
| Amendment | Purpose |
|---|---|
featureAMM |
Core AMM functionality |
fixUniversalNumber |
Required precision improvements |
fixAMMv1_1 |
Rounding bug fixes |
fixAMMv1_2 |
Additional fixes |
fixAMMv1_3 |
Precision loss compensation |
fixAMMOverflowOffer |
Offer generation overflow fix |
featureAMMClawback |
Asset issuer clawback support |
// From AMMCore.cpp
bool ammEnabled(Rules const& rules) {
return rules.enabled(featureAMM) &&
rules.enabled(fixUniversalNumber);
}
Both amendments must be active for AMM functionality to work.
include/xrpl/protocol/detail/ledger_entries.macro - ltAMM definitioninclude/xrpl/ledger/helpers/AMMHelpers.h - LP token calculationssrc/libxrpl/ledger/helpers/AMMHelpers.cpp - Utility functions (formerly AMMUtils)src/libxrpl/tx/transactors/dex/AMMCreate.cpp - Pool creationinclude/xrpl/protocol/detail/features.macro - AMM amendmentsinclude/xrpl/protocol/AMMCore.h - Constants and core definitionsIn brief: the mathematics: invariant, swaps, LP tokens, fees, precision.
The logic behind XRPL's AMM is critical for understanding how prices are determined, LP tokens are calculated, and trades execute. This chapter provides a deep dive into the constant product formula, swap calculations, LP token minting/burning, and the precision handling that ensures numerical correctness.
Location: src/libxrpl/ledger/helpers/AMMHelpers.cpp and AMMHelpers.h
In brief: x times y stays k, and price is the ratio.
XRPL's AMM uses the constant product formula, popularized by Uniswap:
x * y = k
Where:
x = Balance of Asset 1y = Balance of Asset 2k = Constant product (invariant)After every trade, the product of the two asset balances must remain constant (or increase slightly due to fees):
Before trade: A * B = k
After trade: A' * B' >= k
The inequality (>=) accounts for trading fees, which slightly increase k over time, benefiting liquidity providers.
The spot price of Asset1 in terms of Asset2 is:
Price(Asset1) = B / A
As traders buy Asset1:
This creates automatic price discovery through supply and demand.
In brief: exact in and exact out, with the fee in the right place.
Given an input amount, calculate the output amount.
Formula:
out = B - (A * B) / (A + in_fee)
Where:
in_fee = in * (1 - tradingFee / 100000)
Location: include/xrpl/ledger/helpers/AMMHelpers.h:443-505
template <typename TIn, typename TOut>
TOut swapAssetIn(
TAmounts<TIn, TOut> const& pool, // {A, B}
TIn const& assetIn, // Input amount
std::uint16_t tfee) // Trading fee
{
// Calculate fee-adjusted input
auto const in_fee = assetIn * feeMult(tfee);
// Apply constant product formula
// out = B - (A * B) / (A + in_fee)
auto const out = pool.out -
divide(pool.in * pool.out, pool.in + in_fee, ...);
return out;
}
Example:
Pool: 1000 XRP / 2000 USD
Trading Fee: 0.3% (tfee = 300, since the TradingFee field's 1000 = 1%)
Input: 100 XRP
in_fee = 100 * (1 - 300/100000) = 100 * 0.997 = 99.7 XRP
out = 2000 - (1000 * 2000) / (1000 + 99.7)
out = 2000 - 2000000 / 1099.7
out = 2000 - 1818.68
out = 181.32 USD
Given a desired output amount, calculate the required input.
Formula:
in = ((A * B) / (B - out) - A) / (1 - fee)
Location: include/xrpl/ledger/helpers/AMMHelpers.h:517-578
template <typename TIn, typename TOut>
TIn swapAssetOut(
TAmounts<TIn, TOut> const& pool, // {A, B}
TOut const& assetOut, // Desired output
std::uint16_t tfee) // Trading fee
{
// Reverse constant product calculation
auto const in = divide(
pool.in * pool.out / (pool.out - assetOut) - pool.in,
feeMult(tfee),
...);
return in;
}
Critical Design Decision: Rounding always favors the AMM pool.
swapAssetIn: Output is rounded down (less tokens out)swapAssetOut: Input is rounded up (more tokens required)This ensures the pool never loses value due to rounding errors.
In brief: minting and burning shares of the pool.
When creating a pool, initial LP tokens are the geometric mean:
LPT = sqrt(Asset1 * Asset2)
Location: src/libxrpl/ledger/helpers/AMMHelpers.cpp:5-17
STAmount ammLPTokens(
STAmount const& asset1,
STAmount const& asset2,
Issue const& lptIssue)
{
// Geometric mean ensures equal value contribution
auto const tokens = root2(asset1 * asset2);
return toSTAmount(lptIssue, tokens);
}
Why Geometric Mean?
Calculate LP tokens received for depositing assets.
When depositing proportionally (same ratio as pool):
LPT_out = LPT_total * (deposit / balance)
No trading fee charged for proportional deposits.
Equation 3 from AMMHelpers.cpp:
t = T * [(b/B - (sqrt(f2^2 - b/(B*f1)) - f2)) / (1 + sqrt(f2^2 - b/(B*f1)) - f2)]
Where:
t = LP tokens receivedT = Total LP tokensb = Deposit amountB = Pool balance of deposited assetf1 = 1 - tradingFeef2 = (1 - tradingFee/2) / f1Location: include/xrpl/ledger/helpers/AMMHelpers.h:131-187
template <typename T>
STAmount lpTokensOut(
STAmount const& asset, // Pool balance B
STAmount const& lptAMMBalance, // Total LP tokens T
T const& depositAmount, // Deposit amount b
std::uint16_t tfee) // Trading fee
{
// Apply Equation 3
auto const f1 = feeMult(tfee);
auto const f2 = feeMultHalf(tfee) / f1;
auto const ratio = depositAmount / asset;
auto const sqrt_term = root2(f2 * f2 - ratio / f1) - f2;
auto const lpTokens = lptAMMBalance * (ratio - sqrt_term) / (1 + sqrt_term);
return lpTokens;
}
Calculate how much asset is needed for specific LP tokens.
Equation 4 (inverse of Equation 3):
template <typename T>
T ammAssetIn(
STAmount const& asset,
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
std::uint16_t tfee)
{
// Inverse calculation to find required deposit
// for desired LP tokens output
}
Calculate LP tokens needed for a specific withdrawal.
Equation 7:
t = T * (c - sqrt(c^2 - 4*R)) / 2
Where:
t = LP tokens to burnT = Total LP tokensR = withdrawal / poolBalancec = R * fee + 2 - feeCalculate assets received for burning LP tokens.
Equation 8:
template <typename T>
T ammAssetOut(
STAmount const& asset,
STAmount const& lptAMMBalance,
STAmount const& lpTokens,
std::uint16_t tfee)
{
// Calculate withdrawal amount for given LP tokens
}
In brief: how the trading fee enters every formula.
// Full fee multiplier: 1 - fee
Number feeMult(std::uint16_t tfee)
{
return 1 - Number(tfee) / AUCTION_SLOT_FEE_SCALE_FACTOR;
}
// Half fee multiplier: 1 - fee/2 (for proportional operations)
Number feeMultHalf(std::uint16_t tfee)
{
return 1 - Number(tfee) / (2 * AUCTION_SLOT_FEE_SCALE_FACTOR);
}
// Get fee as decimal
Number getFee(std::uint16_t tfee)
{
return Number(tfee) / AUCTION_SLOT_FEE_SCALE_FACTOR;
}
Constants:
constexpr std::uint32_t AUCTION_SLOT_FEE_SCALE_FACTOR = 100000;
Auction slot holders pay reduced fees:
std::uint16_t getTradingFee(
ReadView const& view,
SLE const& ammSle,
AccountID const& account)
{
// Check if account is slot holder or authorized
if (isAuctionSlotHolder(ammSle, account))
return ammSle[sfAuctionSlot].getFieldU16(sfDiscountedFee); // Usually 0
return ammSle.getFieldU16(sfTradingFee);
}
In brief: spot price, quality, and CLOB comparability.
The "quality" represents the exchange rate for offers:
Quality = TakerGets / TakerPays
For AMM, the spot price quality is:
SpotQuality = PoolOut / PoolIn
The pathfinding engine needs AMM offers that match CLOB quality.
Location: include/xrpl/ledger/helpers/AMMHelpers.h:310-420
template <typename TIn, typename TOut>
std::optional<TAmounts<TIn, TOut>>
changeSpotPriceQuality(
TAmounts<TIn, TOut> const& pool,
Quality const& quality, // Target quality (from CLOB)
std::uint16_t tfee,
Rules const& rules,
beast::Journal j)
{
// Solve quadratic equation to find offer amounts
// that result in pool having target spot price quality
// after the trade
// This enables AMM to compete with CLOB offers
}
In brief: Number, rounding rules, and invariant checks.
XRPL uses a custom Number type for AMM calculations:
// Number provides arbitrary precision arithmetic
// Essential for avoiding overflow in large pool calculations
Number result = root2(asset1 * asset2);
Several amendments improve precision handling:
| Amendment | Fix |
|---|---|
| fixUniversalNumber | Required for AMM (precision improvements) |
| fixAMMv1_1 | Rounding improvements |
| fixAMMv1_3 | Precision loss compensation |
| fixAMMOverflowOffer | Overflow in offer calculation |
// Round based on who should benefit
enum class Round {
upward, // Round up (more required from user)
downward // Round down (less given to user)
};
// Swaps: Round to favor pool
// Deposits: Round to favor pool
// Withdrawals: Round to favor pool
After every operation, verify the pool invariant:
bool checkInvariant(
TAmounts<TIn, TOut> const& oldPool,
TAmounts<TIn, TOut> const& newPool)
{
// New product must be >= old product
return (newPool.in * newPool.out) >= (oldPool.in * oldPool.out);
}
In brief: the formulas with real numbers plugged in.
Pool: 10,000 XRP / 20,000 USD
Fee: 0.3% (TradingFee 300)
Swap: 500 XRP -> USD
Step 1: Apply fee
in_fee = 500 * (1 - 0.003) = 498.5 XRP
Step 2: Calculate output
k = 10000 * 20000 = 200,000,000
new_xrp = 10000 + 498.5 = 10498.5
new_usd = k / new_xrp = 200000000 / 10498.5 = 19050.45
out = 20000 - 19050.45 = 949.55 USD
Result: 500 XRP -> 949.55 USD
Effective rate: 1.899 USD/XRP
Pool: 10,000 XRP / 20,000 USD
Total LP Tokens: 14,142.13 (sqrt(10000 * 20000))
Deposit: 1,000 XRP + 2,000 USD (proportional)
Ratio = 1000/10000 = 0.1 (10%)
LP_out = 14142.13 * 0.1 = 1,414.21 LP tokens
New state:
- Pool: 11,000 XRP / 22,000 USD
- Total LP: 15,556.34
- Depositor owns: 1,414.21 / 15,556.34 = 9.09% of pool
Pool: 10,000 XRP / 20,000 USD
Total LP: 14,142.13
Fee: 0.3%
Deposit: 1,000 XRP only
Using Equation 3:
f1 = 1 - 0.003 = 0.997
f2 = (1 - 0.0015) / 0.997 = 1.0005
ratio = 1000 / 10000 = 0.1
sqrt_term = sqrt(1.0005^2 - 0.1/0.997) - 1.0005
= sqrt(1.001 - 0.1003) - 1.0005
= sqrt(0.9007) - 1.0005
= 0.949 - 1.0005
= -0.0515
LP_out = 14142.13 * (0.1 - (-0.0515)) / (1 + (-0.0515))
= 14142.13 * 0.1515 / 0.9485
= 2259.3 LP tokens
Note: Single asset deposit gets fewer LP tokens
due to implicit swap + fee
src/libxrpl/ledger/helpers/AMMHelpers.cpp - Core calculationsinclude/xrpl/ledger/helpers/AMMHelpers.h - Formula implementationsinclude/xrpl/protocol/AMMCore.h - Constantssrc/libxrpl/basics/Number.cpp - Precision arithmeticThis module opened up the Automated Market Maker built into the ledger. You learned the ltAMM ledger entry that stores a pool, the pseudo-account that holds the pool's funds with no usable signing key, the LP tokens that represent each provider's share, and the constant-product maths that prices every swap, along with the vote and auction slots for fee governance and discounts.
To remember:
ltAMM entry stores: the two assets, the LP token issue, TradingFee, VoteSlots, AuctionSlotx * y = k; trading fees accrue into the poolkeylet::amm(asset, asset2) is order-independentsrc/libxrpl/tx/transactors/dex + include/xrpl/protocol/AMMCore.hNext up. You know the AMM's anatomy; now watch it trade. Next: the AMM transactions and its integration with pathfinding.
Resources
Assignments
0 of 2 complete