From 46d1add256b2bc4b3601c879ed12cc4d1c0d1311 Mon Sep 17 00:00:00 2001 From: mDuo13 Date: Thu, 2 May 2024 15:37:41 -0700 Subject: [PATCH] Auction slot: update sample code --- _code-samples/auction-slot/js/auction-slot.js | 204 +++++++++++++----- 1 file changed, 154 insertions(+), 50 deletions(-) diff --git a/_code-samples/auction-slot/js/auction-slot.js b/_code-samples/auction-slot/js/auction-slot.js index eb1e4e07f7..d5b6968f96 100644 --- a/_code-samples/auction-slot/js/auction-slot.js +++ b/_code-samples/auction-slot/js/auction-slot.js @@ -65,8 +65,10 @@ function swapOut(asset_out_bn, pool_in_bn, pool_out_bn, trading_fee) { * 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)); + const b2minus4ac = b.multipliedBy(b).minus( + a.multipliedBy(c).multipliedBy(4) + ) + return ( b.negated().plus(b2minus4ac.sqrt()) ).dividedBy(a.multipliedBy(2)) } /* @@ -80,7 +82,6 @@ function solveQuadraticEq(a,b,c) { * represents a 1% fee. */ function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { - // TODO: refactor to take pool_in as a BigNumber so precision can be set based on XRP/drops? // convert inputs to BigNumber const lpTokens = BigNumber(desired_lpt) const lptAMMBalance = BigNumber(lpt_balance) @@ -92,43 +93,85 @@ function ammAssetIn(pool_in, lpt_balance, desired_lpt, trading_fee) { 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 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)) } /* * Calculates the necessary bid to win the AMM Auction slot, per the pricing - * algorithm defined in XLS-30 section 4.1.1. + * algorithm defined in XLS-30 section 4.1.1, if you already hold LP Tokens. + * Not useful in the case where you need to make a deposit to get LP Tokens, + * because doing so causes more LP Tokens to be issued, changing the min bid. * @returns BigNumber - the minimum amount of LP tokens to win the auction slot */ function auctionPrice(old_bid, time_interval, trading_fee, lpt_balance) { const tfee_decimal = feeDecimal(trading_fee) - const min_bid = BigNumber(lpt_balance).multipliedBy(tfee_decimal).dividedBy(25) + const lptokens = BigNumber(lpt_balance) + const min_bid = lptokens.multipliedBy(tfee_decimal).dividedBy(25) const b = BigNumber(old_bid) - if (time_interval >= 20) { - return min_bid - - } else if (time_interval > 1) { - const t60 = BigNumber("0.05").multipliedBy(time_interval).exponentiatedBy(60) - return b.multipliedBy("1.05").multipliedBy(BigNumber(1).minus(t60)).plus(min_bid) - - } else { // time_interval <= 1 - return b.multipliedBy(BigNumber("1.05")).plus(min_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 } + +/* + * Calculates 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. + */ +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 +} + + async function main() { // Connect ---------------------------------------------------------------- - const client = new xrpl.Client('wss://s.devnet.rippletest.net:51233') + const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233') console.log("Connecting to Testnet...") await client.connect() - // Get credentials from the Testnet Faucet -------------------------------- - console.log("Requesting address from the Testnet faucet...") + // // Get credentials from the faucet ------------------------------------- + console.log("Requesting test XRP from the faucet...") const wallet = (await client.fundWallet()).wallet - console.log(`Got address ${wallet.address}.`) + console.log(`Got address ${wallet.address} / seed ${wallet.seed}.`) // Look up the AMM const from_asset = { @@ -138,76 +181,123 @@ async function main() { "currency": "TST", "issuer": "rP9jPyP5kyvFRb6ZiRghAGw5u8SGAmU4bd" } - - const amm_info = (await client.request({"command": "amm_info", "asset": from_asset, "asset2": to_asset})) + const amm_info = (await client.request({ + "command": "amm_info", + "asset": from_asset, + "asset2": to_asset + })) console.dir(amm_info, {depth: null}) - const amm_account = amm_info.result.amm.account const lpt = amm_info.result.amm.lp_token - const pool_drops = amm_info.result.amm.amount // XRP is always first if the pool is token←→XRP + // 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_trading_fee = amm_info.result.amm.auction_slot.discounted_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_interval // Calculate price in XRP to get 10 TST from the AMM ---------------------- - // TODO: first calculate how much will be fulfilled by the order book before getting to 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 ------------ + // 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() + return + } + // 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) - const from_amount = unrounded_amount.dp(0, BigNumber.ROUND_CEIL) // Round XRP to integer drops. - console.log(`Expected cost of ${to_amount.value} ${to_amount.currency}: ${xrpl.dropsToXrp(from_amount)} XRP`) + 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 unrounded_amount_discounted = swapOut(asset_out_bn, pool_in_bn, pool_out_bn, discounted_trading_fee) - const discounted_from_amount = unrounded_amount_discounted.dp(0, BigNumber.ROUND_CEIL) - console.log(`Expected cost with auction slot discount: ${xrpl.dropsToXrp(discounted_from_amount)} XRP`) + 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 - discounted_from_amount + const potential_savings = from_amount.minus(discounted_from_amount) console.log(`Potential savings: ${xrpl.dropsToXrp(potential_savings)} XRP`) + + // Calculate the cost of winning the auction slot, in LP Tokens ----------- - // Calculate the cost of winning the auction slot, in LP Tokens - const auction_price = auctionPrice(old_bid, time_interval, full_trading_fee, lpt.value).precision(15) - console.log(`Auction price: ${auction_price} LP Tokens`) - // Figure out how much XRP we need to deposit to receive the auction price - 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`) + // The price is slightly different if you already hold LP Tokens vs if you + // have to make a deposit, because the deposit causes more LP Tokens to be + // issued, which increases the minimum bid. + const lp_auction_price = auctionPrice(old_bid, time_interval, + full_trading_fee, lpt.value + ).precision(15) + console.log(`Auction price for current LPs: ${lp_auction_price} LP Tokens`) - const SLIPPAGE_MULT = BigNumber(1.01) // allow up to 1% more than estimated amounts. TODO: also allow slippage on auction price? - const deposit_max = deposit_for_bid.multipliedBy(SLIPPAGE_MULT).dp(0).toString() + 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`) + + // 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`) - // TODO: compare price of deposit+bid with potential savings. Don't forget XRP burned as transaction costs + // 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) + // 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() + return + } + + // 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() } - // Do a single-asset deposit to get LP Tokens to bid on the auction slot const deposit_result = await client.submitAndWait({ - // const deposit_autofill = await client.autofill({ "TransactionType": "AMMDeposit", "Account": wallet.address, "Asset": from_asset, "Asset2": to_asset, - "Amount": deposit_max, + "Amount": deposit_max.toString(), "LPTokenOut": auction_bid, "Flags": xrpl.AMMDepositFlags.tfOneAssetLPToken }, {autofill: true, wallet: wallet} @@ -215,19 +305,33 @@ async function main() { console.log("Deposit result:") console.dir(deposit_result, {depth: null}) - - // Bid on the auction slot + // 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 // TODO: try w/ BidMin + BidMax w/ slippage + "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}) + // 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}) + // Done. client.disconnect() } // End of main()