Thsi tutorial shows how to use the auction slot of an Automated Market Maker (AMM) to save the amount of fees you pay when trading against that AMM.
For a simpler example of trading currencies in the XRP Ledger's DEX, see Trade in the Decentralized Exchange.
This tutorial does not exhaustively cover all possible market conditions and circumstances. Always exercise caution and trade at your own risk.
By following this tutorial, you should learn how to:
- Estimate the fees that would be charged by an AMM for a particular trade.
- Estimate the cost of winning an AMM's auction slot.
- Use the AMM's auction slot to pay lower fees.
To complete this tutorial, you should:
Have a basic understanding of the XRP Ledger.
Have an XRP Ledger client library, such as xrpl.js, installed.
Know which AMM you want to trade against: that AMM must already exist in the ledger. For purposes of this tutorial, you can use the following AMM instance that has been set up on the XRP Ledger Testnet in advance:
Currency Code Issuer Notes XRP N/A Testnet XRP is functionally similar to XRP, but holds no real-world value. You can get it for free from a faucet. TST rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bdA test token pegged to XRP at a rate of approximately 10 XRP per 1 TST. The issuer has existing Offers on the XRP Ledger Testnet to buy and sell these tokens. This token has no transfer fee or Tick Size set. For instructions on creating an AMM for a different currency pair, see Create an Automated Market Maker.
See Code Samples: Auction Slot for the full source code for this tutorial.
At a high level, the steps involved in using an AMM auction slot to save money on trading are as follows:
- Step 1: Connect to the ledger so can query the current state.
- Steps 2-4: Estimate how much your desired trade would cost in AMM trading fees.
- Step 5: Compare against the cost to win the current auction slot.
- Steps 6-7: If winning the auction slot is cheaper, use AMMDeposit to acquire some LPTokens and then use AMMBid to spend those tokens on winning the auction slot.
- Step 8: Make the intended trade using an OfferCreate transaction.
For simplicity, this tutorial assumes that you have XRP, you want to acquire a fixed amount of TST in a single trade, and that the entire trade will execute using the AMM. Real-life situations are more complicated. For example, part of your trade may execute by consuming Offers rather than using the AMM, or you may want to do a series of trades over a period of time. If one or both of the assets you are trading has a transfer fee or tick size set, those details can also affect the calculations.
For this use case, you need a high-precision number library such as Bignumber.js for correctly performing calculations on currency amounts you may find in the ledger. The example package.json includes the necessary dependencies:
{
"dependencies": {
"xrpl": "^4.6.0",
"bignumber.js": "^10.0.2"
},
"type": "module"
}From the code sample folder, use npm to install dependencies:
npm iTo estimate the costs of various AMM operations, you need implementations of several AMM functions which match the calculations used by the XRP Ledger. See the Appendix for the implementations of these functions.
Along with the other dependencies, import the AMM helper functions from the file amm-formulas.js:
import xrpl from 'xrpl'
import BigNumber from 'bignumber.js'
import { auctionDeposit, ammAssetIn, swapOut } from "./amm-formulas.js"Instantiate an API client and a Wallet instance for the account to perform the trade:
// Connect and get account ----------------------------------------------------
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233')
console.log("Connecting to Testnet...")
await client.connect()
console.log("Requesting test XRP from the faucet...")
const { wallet } = await client.fundWallet()
console.log(`Got address ${wallet.address} / seed ${wallet.seed}.`)To determine if your trade could benefit from the reduced fees of the AMM auction slot, you must first look up the current state of the AMM. To get the latest information, use the amm_info method and query the current (pending) ledger version.
current ledger incorporates recently-sent transactions that are likely to be confirmed; it is the most up-to-date picture of the ledger state, but the details may change when the ledger is validated. You can also use the validated ledger, which returns only the confirmed data, but the current ledger is usually closer to the state the ledger will be in by the time your trade executes.The following code snippet reads the amm_info result and saves some of the details for later use:
// Look up AMM status -----------------------------------------------------
const from_asset = {
"currency": "XRP"
}
const to_asset = {
"currency": "TST",
"issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd"
}
const amm_info = (await client.request({
"command": "amm_info",
"asset": from_asset,
"asset2": to_asset
}))
console.dir(amm_info, {depth: null})
const lpt = amm_info.result.amm.lp_token
// XRP is always first if the pool is token←→XRP.
// For a token←→token AMM, you'd need to figure out which asset is first.
const pool_drops = amm_info.result.amm.amount
const pool_tst = amm_info.result.amm.amount2
const full_trading_fee = amm_info.result.amm.trading_fee
const discounted_fee = amm_info.result.amm.auction_slot.discounted_fee
const old_bid = amm_info.result.amm.auction_slot.price.value
const time_interval = amm_info.result.amm.auction_slot.time_intervalThis tutorial shows how to estimate the cost, in XRP, to buy a fixed amount of TST using the AMM. The calculations are different for other types of trades, such as a "sell" trade that buys as much TST as possible with a fixed amount of XRP, or for assets with other complications such as transfer fees.
First, define the target amount of TST and check that the AMM can even fulfill the trade:
// Calculate price in XRP to get 10 TST from the AMM ----------------------
// Note, this ignores Offers from the non-AMM part of the DEX.
const to_amount = {
"currency": to_asset.currency,
"issuer": to_asset.issuer,
"value": "10.0"
}
// Convert values to BigNumbers with the appropriate precision.
// Tokens always have 15 significant digits;
// XRP is precise to integer drops, which can be as high as 10^17
const asset_out_bn = BigNumber(to_amount.value).precision(15)
const pool_in_bn = BigNumber(pool_drops).precision(17)
const pool_out_bn = BigNumber(pool_tst.value).precision(15)
if (to_amount.value > pool_out_bn) {
console.log(`Requested ${to_amount.value} ${to_amount.currency} ` +
`but AMM only holds ${pool_tst.value}. Quitting.`)
client.disconnect()
process.exit(1)
}Then, you use the AMM SwapOut formula to calculate how much XRP you need to pay to receive the target amount of TST out. See SwapOut in the Appendix for the implementation of this formula.
To estimate the cost of trading fees, call SwapOut twice: once with the full fee, and once with the discounted fee of the auction slot. The difference between the two represents the maximum possible savings from the auction slot for this trade. The actual savings will be less because of the costs of winning the auction slot.
// Use AMM's SwapOut formula to figure out how much XRP we have to pay
// to receive the target amount of TST, under the current trading fee.
const unrounded_amount = swapOut(asset_out_bn, pool_in_bn,
pool_out_bn, full_trading_fee)
// Round XRP to integer drops. Round ceiling to make you pay in enough.
const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL)
console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ` +
`${xrpl.dropsToXrp(from_amount)} XRP`)
// Same calculation, but assume we have access to the discounted trading
// fee from the auction slot.
const raw_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn,
discounted_fee)
const discounted_from_amount = raw_discounted.dp(0, BigNumber.ROUND_CEIL)
console.log(`Expected cost with auction slot discount: `+
`${xrpl.dropsToXrp(discounted_from_amount)} XRP`)
// The potential savings is the difference between the necessary input
// amounts with the full vs discounted fee.
const potential_savings = from_amount.minus(discounted_from_amount)
console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`)The cost to win the auction slot depends on how long the current holder has held it and how much they paid, but it's always denominated in LP tokens. If you currently only have XRP and you want to win the auction slot, you must first deposit some of your XRP to get LP Tokens.
The price of winning the auction slot is defined in XLS-0030 section 4.1.1. However, the minimum bid scales with the number of LP Tokens issued. If you calculate the auction price and then deposit exactly enough to pay for it, the auction price increases proportionally to the new LP Tokens you gained.
This is similar to cases where you want to deliver exactly $100 after subtracting a 3% fee. If you calculate $100 + (0.03 * $100) = $103, only $99.91 will arrive because the extra $3 is also subject to the fee. Instead, you divide 100 ÷ 0.97 ≈ $103.10 (rounding up to make sure).
The AuctionDeposit formula represents the inverted form of the auction price formula so that you can calulate how much to deposit to match the auction price. See Appendix: AuctionDeposit for the implementation.
You use the function like this:
// Calculate the cost of winning the auction slot, in LP Tokens -----------
const auction_price = auctionDeposit(old_bid, time_interval,
full_trading_fee, lpt.value
).precision(15)
console.log(`Auction price after deposit: ${auction_price} LP Tokens`)The previous step gives a cost for the auction slot in the form of LP Tokens. To compare against your potential savings, you need to convert this to the XRP cost of the deposit. If the XRP cost of making the deposit and winning the auction slot is greater than your savings, then you should not go through with it.
You use the AMM AssetIn formula to estimate how much XRP you have to deposit to receive the target amount of TST. See Appendix: AssetIn for the implementation.
The following code uses AssetIn to estimate the cost of the deposit:
// Calculate how much XRP to deposit to receive that many LP Tokens -------
const deposit_for_bid = ammAssetIn(pool_in_bn, lpt.value, auction_price,
full_trading_fee
).dp(0, BigNumber.ROUND_CEIL)
console.log(`Auction price as XRP single-asset deposit amount: `+
`${xrpl.dropsToXrp(deposit_for_bid)} XRP`)Since the XRP Ledger's decentralized exchange is always open to other traders using it too, new transactions can change the current state at the same time that you are doing these calculations and sending your own transactions. You should allow for some amount of slippage, the change in rates between when you checked them and when your transaction executes. The following example allows up to 1% slippage:
// Optional. Allow for costs to be 1% greater than estimated, in case other
// transactions affect the same AMM during this time.
const SLIPPAGE_MULT = BigNumber(1.01)
const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0)Finally, you take the slippage-adjusted cost in XRP, add the transaction costs in XRP burned for sending two transactions, and compare that total to the potential savings calculated back in step 3. If the total cost is higher than the savings, you won't save money using the auction slot, so you stop here.
// Compare price of deposit+bid with potential savings. -------------------
// Don't forget XRP burned as transaction costs.
const fee_response = (await client.request({"command":"fee"}))
const tx_cost_drops = BigNumber(fee_response.result.drops.minimum_fee
).multipliedBy(client.feeCushion).dp(0)
const net_savings = potential_savings.minus(
tx_cost_drops.multipliedBy(2).plus(deposit_max)
)
if (net_savings > 0) {
console.log(`Estimated net savings from the auction slot: ` +
`${xrpl.dropsToXrp(net_savings)} XRP`)
} else {
console.log(`Estimated the auction slot to be MORE EXPENSIVE by `+
`${xrpl.dropsToXrp(net_savings.negated())} XRP. Quitting.`)
client.disconnect()
process.exit(1)
}Assuming you determined that you could make money, it's now time to send actual transactions to the XRP Ledger, starting with an AMMDeposit transaction to get the LP Tokens that you'll bid on the auction slot.
The "One Asset LP Token" deposit type is most convenient here since you can specify exactly how many LP Tokens you want to receive, and you only need to deposit XRP. The following code creates and sends the transaction:
// Do a single-asset deposit to get LP Tokens to bid on the auction slot --
const auction_bid = {
"currency": lpt.currency,
"issuer": lpt.issuer,
"value": auction_price.toString()
}
const deposit_result = await client.submitAndWait({
"TransactionType": "AMMDeposit",
"Account": wallet.address,
"Asset": from_asset,
"Asset2": to_asset,
"Amount": deposit_max.toString(),
"LPTokenOut": auction_bid,
"Flags": xrpl.AMMDepositFlags.tfOneAssetLPToken
}, {autofill: true, wallet: wallet}
)
console.log("Deposit result:")
console.dir(deposit_result, {depth: null})After the transaction is (or isn't) confirmed by the consensus process, the code displays the results to the console.
Assuming the previous transaction was successful, the next step is to use the LP Tokens to bid on the auction slot. To do this, you send an AMMBid transaction with the slippage-adjusted bid amount you calculated earlier in the BidMax field, as in the following code:
// Actually bid on the auction slot ---------------------------------------
const bid_result = await client.submitAndWait({
"TransactionType": "AMMBid",
"Account": wallet.address,
"Asset": from_asset,
"Asset2": to_asset,
"BidMax": auction_bid,
"BidMin": auction_bid, // So rounding doesn't leave dust amounts of LPT
}, {autofill: true, wallet: wallet}
)
console.log("Bid result:")
console.dir(bid_result, {depth: null})BidMin to the same as BidMax so that you aren't left with a trust line that contains a very tiny amount of LP Tokens that weren't spent on the auction price, and you don't have to meet the XRP reserve for that trust line.If your previous transaction was successful, you should now be the auction slot holder until you are outbid or 24 hours have passed, whichever comes first. You can immediately use this opportunity to make the XRP for TST trade that you wanted to make in the first place. There are several ways to make trades in the XRP Ledger, but sending an OfferCreate transaction is the most conventional, as in the following code:
// Trade using the discount -----------------------------------------------
const spend_drops = discounted_from_amount.multipliedBy(SLIPPAGE_MULT
).dp(0).toString()
const offer_result = await client.submitAndWait({
"TransactionType": "OfferCreate",
"Account": wallet.address,
"TakerPays": to_amount,
"TakerGets": spend_drops
}, {autofill: true, wallet: wallet})
console.log("Offer result:")
console.dir(offer_result, {depth: null})
console.log("Offer balance changes summary:")
console.dir(xrpl.getBalanceChanges(offer_result.result.meta), {depth:null})The above sample code shows how to use various AMM-related formulas in a particular workflow. Your code must also have implementations of those formulas, which can be in the same file or imported from a separate one as you desire.
The file amm-formulas.js contains the source code for all these formulas.
The AssetIn formula calculates how much of an asset must be deposited to receive a target amount of LP Tokens.
The following function implements AssetIn in JavaScript:
/* Implement the AMM single-asset deposit formula to calculate how much to
* put in so that you receive a specific number of LP Tokens back.
* C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/impl/AMMHelpers.cpp#L55-L83
* @param pool_in string - Quantity of input asset the pool already has
* @param lpt_balance string - Quantity of LP Tokens already issued by the AMM
* @param desired_lpt string - Quantity of new LP Tokens you want to receive
* @param trading_fee int - The trading fee as an integer {0,1000} where 1000
* represents a 1% fee.
*/
export function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) {
// convert inputs to BigNumber
const lpTokens = BigNumber(desired_lpt)
const lptAMMBalance = BigNumber(lpt_balance)
const asset1Balance = BigNumber(pool_in)
const f1 = feeMult(trading_fee)
const f2 = feeMultHalf(trading_fee).dividedBy(f1)
const t1 = lpTokens.dividedBy(lptAMMBalance)
const t2 = t1.plus(1)
const d = f2.minus( t1.dividedBy(t2) )
const a = BigNumber(1).dividedBy( t2.multipliedBy(t2))
const b = BigNumber(2).multipliedBy(d).dividedBy(t2).minus(
BigNumber(1).dividedBy(f1)
)
const c = d.multipliedBy(d).minus( f2.multipliedBy(f2) )
return asset1Balance.multipliedBy(solveQuadraticEq(a,b,c))
}This function depends on the feeMult, feeMultHalf, and solveQuadraticEq helper functions.
The AuctionDeposit function is an inverted form of the formula for the AMM's auction price, taking into account how the minimum bid value changes as LP Tokens are issued. AuctionDeposit calculates the total amount of LP Tokens you need to receive in a deposit if you want to match the auction price:
/* Calculate how much to deposit, in terms of LP Tokens out, to be able to win
* the auction slot. This is based on the slot pricing algorithm defined in
* XLS-30 section 4.1.1, but factors in the increase in the minimum bid as a
* result of having new LP Tokens issued to you from your deposit.
*/
export function auctionDeposit(old_bid, time_interval, trading_fee, lpt_balance) {
const tfee_decimal = feeDecimal(trading_fee)
const lptokens = BigNumber(lpt_balance)
const b = BigNumber(old_bid)
let outbidAmount = BigNumber(0) // This is the case if time_interval >= 20
if (time_interval == 0) {
outbidAmount = b.multipliedBy("1.05")
} else if (time_interval <= 19) {
const t60 = BigNumber(time_interval).multipliedBy("0.05").exponentiatedBy(60)
outbidAmount = b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60))
}
const new_bid = lptokens.plus(outbidAmount).dividedBy(
BigNumber(25).dividedBy(tfee_decimal).minus(1)
).plus(outbidAmount)
// Significant digits for the deposit are limited by total LPTokens issued
// so we calculate lptokens + deposit - lptokens to determine where the
// rounding occurs. We use ceiling/floor to make sure the amount we receive
// after rounding is still enough to win the auction slot.
const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING
).minus(lptokens).precision(15, BigNumber.FLOOR)
return rounded_bid
}The AuctionPrice function calculates the cost of the auction slot for current LP Token holders. It is not used in this tutorial, which assumes the user does not hold LP Tokens, but is presented here for completeness:
/* Calculate the necessary bid to win the AMM Auction slot, per the pricing
* algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens.
*
* NOT USED in the Auction Slot tutorial, which assumes the user does not hold
* any LP Tokens.
*
* @returns BigNumber - the minimum amount of LP tokens to win the auction slot
*/
export function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) {
const tfee_decimal = feeDecimal(trading_fee)
const lptokens = BigNumber(lpt_balance)
const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25)
const b = BigNumber(old_bid)
let new_bid = min_bid
if (time_interval == 0) {
new_bid = b.multipliedBy("1.05").plus(min_bid)
} else if (time_interval <= 19) {
const t60 = BigNumber(time_interval).multipliedBy("0.05"
).exponentiatedBy(60)
new_bid = b.multipliedBy("1.05").multipliedBy(
BigNumber(1).minus(t60)
).plus(min_bid)
}
const rounded_bid = new_bid.plus(lptokens).precision(15, BigNumber.CEILING
).minus(lptokens).precision(15, BigNumber.FLOOR)
return rounded_bid
}The SwapOut formula, defined in the XRPL-0030 specification as formula 10, calculates how much of one asset you have to swap in to the AMM to receive a target amount of the other asset out from the AMM.
The following function implements SwapOut in JavaScript:
/* Implement the AMM SwapOut formula, as defined in XLS-30 section 2.4 AMM
* Swap, formula 10. The asset weights WA/WB are currently always 1/1 so
* they're canceled out.
* C++ source: https://github.com/XRPLF/rippled/blob/2d1854f354ff8bb2b5671fd51252c5acd837c433/src/ripple/app/misc/AMMHelpers.h#L253-L258
* @param asset_out_bn BigNumber - The target amount to receive from the AMM.
* @param pool_in_bn BigNumber - The amount of the input asset in the AMM's
* pool before the swap.
* @param pool_out_bn BigNumber - The amount of the output asset in the AMM's
* pool before the swap.
* @param trading_fee int - The trading fee as an integer {0, 1000} where 1000
* represents a 1% fee.
* @returns BigNumber - The amount of the input asset that must be swapped in
* to receive the target output amount. Unrounded, because
* the number of decimals depends on if this is drops of
* XRP or a decimal amount of a token; since this is a
* theoretical input to the pool, it should be rounded
* up (ceiling) to preserve the pool's constant product.
*/
export function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) {
return ( ( pool_in_bn.multipliedBy(pool_out_bn) ).dividedBy(
pool_out_bn.minus(asset_out_bn)
).minus(pool_in_bn)
).dividedBy(feeMult(trading_fee))
}These helper functions are used in other AMM formulas to convert from a trading fee value in the ledger (integer from 0 to 1000) to a decimal representation that can be multiplied by a total to apply the fee.
The following functions implement feeMult, feeMultHalf, and feeDecimal in JavaScript:
/* Convert a trading fee to a value that can be multiplied
* by a total to "subtract" the fee from the total.
* @param tFee int {0, 1000}
* such that 1 = 1/100,000 and 1000 = 1% fee
* @returns BigNumber (1 - fee) as a decimal
*/
function feeMult(tFee) {
return BigNumber(1).minus( feeDecimal(tFee) )
}
/* Same as feeMult, but with half the trading fee. Single-asset deposits and
* withdrawals use this because half of the deposit is treated as being
* "swapped" for the other asset in the AMM's pool.
* @param tFee int {0, 1000}
* such that 1 = 1/100,000 and 1000 = 1% fee
* @returns BigNumber (1 - (fee/2)) as a decimal
*/
function feeMultHalf(tFee) {
return BigNumber(1).minus( feeDecimal(tFee).dividedBy(2) )
}
/* Convert a trading fee to a decimal BigNumber value,
* for example 1000 becomes 0.01
* @param tFee int {0, 1000}
* such that 1 = 1/100,000 and 1000 = 1% fee
* @returns BigNumber(fee) as a decimal
*/
function feeDecimal(tFee) {
const AUCTION_SLOT_FEE_SCALE_FACTOR = 100000
return BigNumber(tFee).dividedBy(AUCTION_SLOT_FEE_SCALE_FACTOR)
}The AssetIn and SwapOut functions depend on these helper functions.
This helper function implements the quadratic equation in JavaScript:
/* Compute the quadratic formula. Helper function for ammAssetIn.
* Params and return value are BigNumber instances.
*/
function solveQuadraticEq(a,b,c) {
const b2minus4ac = b.multipliedBy(b).minus(
a.multipliedBy(c).multipliedBy(4)
)
return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2))
}The AssetIn formula depends on this.
Trading assets is never risk-free, and Automated Market Makers are no exception. However, the XRP Ledger's fast, low-cost transactions can be helpful in reducing the costs and fees associated with currency exchange. This tutorial demonstrates a minimal approach to using the auction slot to save money, but of course more creative uses are possible.
The details depend on your specific circumstances and the types of trades you are doing, or expect to do in the future, as well as the state of the market, the XRP Ledger network, and the AMM instances in particular. See the Code Samples for additional related use cases, and feel free to contribute your own samples as well to show the community what can be done on the XRP Ledger.