diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 53a5e61a7b7..b676c5ff5e9 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -74,6 +74,7 @@ target_sources (xrpl_core PRIVATE subdir: protocol #]===============================] src/ripple/protocol/impl/AccountID.cpp + src/ripple/protocol/impl/AMMCore.cpp src/ripple/protocol/impl/Book.cpp src/ripple/protocol/impl/BuildInfo.cpp src/ripple/protocol/impl/ErrorCodes.cpp @@ -81,10 +82,12 @@ target_sources (xrpl_core PRIVATE src/ripple/protocol/impl/Indexes.cpp src/ripple/protocol/impl/InnerObjectFormats.cpp src/ripple/protocol/impl/Issue.cpp + src/ripple/protocol/impl/STIssue.cpp src/ripple/protocol/impl/Keylet.cpp src/ripple/protocol/impl/LedgerFormats.cpp src/ripple/protocol/impl/PublicKey.cpp src/ripple/protocol/impl/Quality.cpp + src/ripple/protocol/impl/QualityFunction.cpp src/ripple/protocol/impl/Rate2.cpp src/ripple/protocol/impl/Rules.cpp src/ripple/protocol/impl/SField.cpp @@ -223,6 +226,7 @@ install ( install ( FILES src/ripple/protocol/AccountID.h + src/ripple/protocol/AMMCore.h src/ripple/protocol/AmountConversions.h src/ripple/protocol/Book.h src/ripple/protocol/BuildInfo.h @@ -239,12 +243,14 @@ install ( src/ripple/protocol/Protocol.h src/ripple/protocol/PublicKey.h src/ripple/protocol/Quality.h + src/ripple/protocol/QualityFunction.h src/ripple/protocol/Rate.h src/ripple/protocol/Rules.h src/ripple/protocol/SField.h src/ripple/protocol/SOTemplate.h src/ripple/protocol/STAccount.h src/ripple/protocol/STAmount.h + src/ripple/protocol/STIssue.h src/ripple/protocol/STArray.h src/ripple/protocol/STBase.h src/ripple/protocol/STBitString.h @@ -423,6 +429,8 @@ target_sources (rippled PRIVATE src/ripple/app/reporting/ReportingETL.cpp src/ripple/app/reporting/ETLSource.cpp src/ripple/app/reporting/P2pProxy.cpp + src/ripple/app/misc/impl/AMMHelpers.cpp + src/ripple/app/misc/impl/AMMUtils.cpp src/ripple/app/misc/CanonicalTXSet.cpp src/ripple/app/misc/FeeVoteImpl.cpp src/ripple/app/misc/HashRouter.cpp @@ -448,6 +456,8 @@ target_sources (rippled PRIVATE src/ripple/app/paths/RippleCalc.cpp src/ripple/app/paths/RippleLineCache.cpp src/ripple/app/paths/TrustLine.cpp + src/ripple/app/paths/impl/AMMLiquidity.cpp + src/ripple/app/paths/impl/AMMOffer.cpp src/ripple/app/paths/impl/BookStep.cpp src/ripple/app/paths/impl/DirectStep.cpp src/ripple/app/paths/impl/PaySteps.cpp @@ -464,6 +474,11 @@ target_sources (rippled PRIVATE src/ripple/app/rdb/impl/UnitaryShard.cpp src/ripple/app/rdb/impl/Vacuum.cpp src/ripple/app/rdb/impl/Wallet.cpp + src/ripple/app/tx/impl/AMMBid.cpp + src/ripple/app/tx/impl/AMMCreate.cpp + src/ripple/app/tx/impl/AMMDeposit.cpp + src/ripple/app/tx/impl/AMMVote.cpp + src/ripple/app/tx/impl/AMMWithdraw.cpp src/ripple/app/tx/impl/ApplyContext.cpp src/ripple/app/tx/impl/BookTip.cpp src/ripple/app/tx/impl/CancelCheck.cpp @@ -629,6 +644,7 @@ target_sources (rippled PRIVATE src/ripple/rpc/handlers/AccountObjects.cpp src/ripple/rpc/handlers/AccountOffers.cpp src/ripple/rpc/handlers/AccountTx.cpp + src/ripple/rpc/handlers/AMMInfo.cpp src/ripple/rpc/handlers/BlackList.cpp src/ripple/rpc/handlers/BookOffers.cpp src/ripple/rpc/handlers/CanDelete.cpp @@ -735,6 +751,9 @@ if (tests) src/test/app/AccountDelete_test.cpp src/test/app/AccountTxPaging_test.cpp src/test/app/AmendmentTable_test.cpp + src/test/app/AMM_test.cpp + src/test/app/AMMCalc_test.cpp + src/test/app/AMMExtended_test.cpp src/test/app/Check_test.cpp src/test/app/Clawback_test.cpp src/test/app/CrossingLimits_test.cpp @@ -877,9 +896,12 @@ if (tests) src/test/jtx/Env_test.cpp src/test/jtx/WSClient_test.cpp src/test/jtx/impl/Account.cpp + src/test/jtx/impl/AMM.cpp + src/test/jtx/impl/AMMTest.cpp src/test/jtx/impl/Env.cpp src/test/jtx/impl/JSONRPCClient.cpp src/test/jtx/impl/ManualTimeKeeper.cpp + src/test/jtx/impl/TestHelpers.cpp src/test/jtx/impl/WSClient.cpp src/test/jtx/impl/acctdelete.cpp src/test/jtx/impl/account_txn_id.cpp @@ -995,6 +1017,7 @@ if (tests) src/test/rpc/AccountSet_test.cpp src/test/rpc/AccountTx_test.cpp src/test/rpc/AmendmentBlocked_test.cpp + src/test/rpc/AMMInfo_test.cpp src/test/rpc/Book_test.cpp src/test/rpc/DepositAuthorized_test.cpp src/test/rpc/DeliveredAmount_test.cpp diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index 401040fc2d7..79dcdd3cc0f 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -142,6 +142,8 @@ test.jtx > ripple.json test.jtx > ripple.ledger test.jtx > ripple.net test.jtx > ripple.protocol +test.jtx > ripple.resource +test.jtx > ripple.rpc test.jtx > ripple.server test.ledger > ripple.app test.ledger > ripple.basics diff --git a/src/ripple/app/ledger/OrderBookDB.cpp b/src/ripple/app/ledger/OrderBookDB.cpp index 343e7f6269a..45262c4d8a9 100644 --- a/src/ripple/app/ledger/OrderBookDB.cpp +++ b/src/ripple/app/ledger/OrderBookDB.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -93,7 +94,7 @@ OrderBookDB::update(std::shared_ptr const& ledger) JLOG(j_.debug()) << "Beginning update (" << ledger->seq() << ")"; - // walk through the entire ledger looking for orderbook entries + // walk through the entire ledger looking for orderbook/AMM entries int cnt = 0; try @@ -126,6 +127,21 @@ OrderBookDB::update(std::shared_ptr const& ledger) ++cnt; } + else if (sle->getType() == ltAMM) + { + auto const issue1 = (*sle)[sfAsset]; + auto const issue2 = (*sle)[sfAsset2]; + auto addBook = [&](Issue const& in, Issue const& out) { + allBooks[in].insert(out); + + if (isXRP(out)) + xrpBooks.insert(in); + + ++cnt; + }; + addBook(issue1, issue2); + addBook(issue2, issue1); + } } } catch (SHAMapMissingNode const& mn) diff --git a/src/ripple/app/misc/AMMHelpers.h b/src/ripple/app/misc/AMMHelpers.h new file mode 100644 index 00000000000..24c25922800 --- /dev/null +++ b/src/ripple/app/misc/AMMHelpers.h @@ -0,0 +1,311 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED +#define RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace ripple { + +/** Calculate LP Tokens given AMM pool reserves. + * @param asset1 AMM one side of the pool reserve + * @param asset2 AMM another side of the pool reserve + * @return LP Tokens as IOU + */ +STAmount +ammLPTokens( + STAmount const& asset1, + STAmount const& asset2, + Issue const& lptIssue); + +/** Calculate LP Tokens given asset's deposit amount. + * @param asset1Balance current AMM asset1 balance + * @param asset1Deposit requested asset1 deposit amount + * @param lptAMMBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return tokens + */ +STAmount +lpTokensIn( + STAmount const& asset1Balance, + STAmount const& asset1Deposit, + STAmount const& lptAMMBalance, + std::uint16_t tfee); + +/** Calculate asset deposit given LP Tokens. + * @param asset1Balance current AMM asset1 balance + * @param lpTokens LP Tokens + * @param lptAMMBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return + */ +STAmount +ammAssetIn( + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee); + +/** Calculate LP Tokens given asset's withdraw amount. Return 0 + * if can't calculate. + * @param asset1Balance current AMM asset1 balance + * @param asset1Withdraw requested asset1 withdraw amount + * @param lptAMMBalance AMM LPT balance + * @param tfee trading fee in basis points + * @return tokens out amount + */ +STAmount +lpTokensOut( + STAmount const& asset1Balance, + STAmount const& asset1Withdraw, + STAmount const& lptAMMBalance, + std::uint16_t tfee); + +/** Calculate asset withdrawal by tokens + * @param assetBalance balance of the asset being withdrawn + * @param lptAMMBalance total AMM Tokens balance + * @param lpTokens LP Tokens balance + * @param tfee trading fee in basis points + * @return calculated asset amount + */ +STAmount +withdrawByTokens( + STAmount const& assetBalance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee); + +/** Check if the relative distance between the qualities + * is within the requested distance. + * @param calcQuality calculated quality + * @param reqQuality requested quality + * @param dist requested relative distance + * @return true if within dist, false otherwise + */ +inline bool +withinRelativeDistance( + Quality const& calcQuality, + Quality const& reqQuality, + Number const& dist) +{ + if (calcQuality == reqQuality) + return true; + auto const [min, max] = std::minmax(calcQuality, reqQuality); + // Relative distance is (max - min)/max. Can't use basic operations + // on Quality. Have to use Quality::rate() instead, which + // is inverse of quality: (1/max.rate - 1/min.rate)/(1/max.rate) + return ((min.rate() - max.rate()) / min.rate()) < dist; +} + +/** Check if the relative distance between the amounts + * is within the requested distance. + * @param calc calculated amount + * @param req requested amount + * @param dist requested relative distance + * @return true if within dist, false otherwise + */ +// clang-format off +template + requires( + std::is_same_v || std::is_same_v || + std::is_same_v) +bool +withinRelativeDistance(Amt const& calc, Amt const& req, Number const& dist) +{ + if (calc == req) + return true; + auto const [min, max] = std::minmax(calc, req); + return ((max - min) / max) < dist; +} +// clang-format on + +/** Finds takerPays (i) and takerGets (o) such that given pool composition + * poolGets(I) and poolPays(O): (O - o) / (I + i) = quality. + * Where takerGets is calculated as the swapAssetIn (see below). + * The above equation produces the quadratic equation: + * i^2*(1-fee) + i*I*(2-fee) + I^2 - I*O/quality, + * which is solved for i, and o is found with swapAssetIn(). + * @param pool AMM pool balances + * @param quality requested quality + * @param tfee trading fee in basis points + * @return seated in/out amounts if the quality can be changed + */ +template +std::optional> +changeSpotPriceQuality( + TAmounts const& pool, + Quality const& quality, + std::uint16_t tfee) +{ + auto const f = feeMult(tfee); // 1 - fee + auto const& a = f; + auto const b = pool.in * (1 + f); + Number const c = pool.in * pool.in - pool.in * pool.out * quality.rate(); + if (auto const res = b * b - 4 * a * c; res < 0) + return std::nullopt; + else if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); + nTakerPaysPropose > 0) + { + auto const nTakerPays = [&]() { + // The fee might make the AMM offer quality less than CLOB quality. + // Therefore, AMM offer has to satisfy this constraint: o / i >= q. + // Substituting o with swapAssetIn() gives: + // i <= O / q - I / (1 - fee). + auto const nTakerPaysConstraint = + pool.out * quality.rate() - pool.in / f; + if (nTakerPaysPropose > nTakerPaysConstraint) + return nTakerPaysConstraint; + return nTakerPaysPropose; + }(); + if (nTakerPays <= 0) + return std::nullopt; + auto const takerPays = toAmount( + getIssue(pool.in), nTakerPays, Number::rounding_mode::upward); + // should not fail + if (auto const amounts = + TAmounts{ + takerPays, swapAssetIn(pool, takerPays, tfee)}; + Quality{amounts} < quality && + !withinRelativeDistance(Quality{amounts}, quality, Number(1, -7))) + Throw("changeSpotPriceQuality failed"); + else + return amounts; + } + return std::nullopt; +} + +/** AMM pool invariant - the product (A * B) after swap in/out has to remain + * at least the same: (A + in) * (B - out) >= A * B + * XRP round-off may result in a smaller product after swap in/out. + * To address this: + * - if on swapIn the out is XRP then the amount is round-off + * downward, making the product slightly larger since out + * value is reduced. + * - if on swapOut the in is XRP then the amount is round-off + * upward, making the product slightly larger since in + * value is increased. + */ + +/** Swap assetIn into the pool and swap out a proportional amount + * of the other asset. Implements AMM Swap in. + * @see [XLS30d:AMM + * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) + * @param pool current AMM pool balances + * @param assetIn amount to swap in + * @param tfee trading fee in basis points + * @return + */ +template +TOut +swapAssetIn( + TAmounts const& pool, + TIn const& assetIn, + std::uint16_t tfee) +{ + return toAmount( + getIssue(pool.out), + pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)), + Number::rounding_mode::downward); +} + +/** Swap assetOut out of the pool and swap in a proportional amount + * of the other asset. Implements AMM Swap out. + * @see [XLS30d:AMM + * Swap](https://github.com/XRPLF/XRPL-Standards/discussions/78) + * @param pool current AMM pool balances + * @param assetOut amount to swap out + * @param tfee trading fee in basis points + * @return + */ +template +TIn +swapAssetOut( + TAmounts const& pool, + TOut const& assetOut, + std::uint16_t tfee) +{ + return toAmount( + getIssue(pool.in), + ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / + feeMult(tfee), + Number::rounding_mode::upward); +} + +/** Return square of n. + */ +Number +square(Number const& n); + +/** Adjust LP tokens to deposit/withdraw. + * Amount type keeps 16 digits. Maintaining the LP balance by adding deposited + * tokens or subtracting withdrawn LP tokens from LP balance results in + * losing precision in LP balance. I.e. the resulting LP balance + * is less than the actual sum of LP tokens. To adjust for this, subtract + * old tokens balance from the new one for deposit or vice versa for withdraw + * to cancel out the precision loss. + * @param lptAMMBalance LPT AMM Balance + * @param lpTokens LP tokens to deposit or withdraw + * @param isDeposit true if deposit, false if withdraw + */ +STAmount +adjustLPTokens( + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + bool isDeposit); + +/** Calls adjustLPTokens() and adjusts deposit or withdraw amounts if + * the adjusted LP tokens are less than the provided LP tokens. + * @param amountBalance asset1 pool balance + * @param amount asset1 to deposit or withdraw + * @param amount2 asset2 to deposit or withdraw + * @param lptAMMBalance LPT AMM Balance + * @param lpTokens LP tokens to deposit or withdraw + * @param tfee trading fee in basis points + * @param isDeposit true if deposit, false if withdraw + * @return + */ +std::tuple, STAmount> +adjustAmountsByLPTokens( + STAmount const& amountBalance, + STAmount const& amount, + std::optional const& amount2, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee, + bool isDeposit); + +/** Positive solution for quadratic equation: + * x = (-b + sqrt(b**2 + 4*a*c))/(2*a) + */ +Number +solveQuadraticEq(Number const& a, Number const& b, Number const& c); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED diff --git a/src/ripple/app/misc/AMMUtils.h b/src/ripple/app/misc/AMMUtils.h new file mode 100644 index 00000000000..1718df496b8 --- /dev/null +++ b/src/ripple/app/misc/AMMUtils.h @@ -0,0 +1,98 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#ifndef RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED +#define RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED + +#include +#include +#include +#include +#include +#include + +namespace ripple { + +class ReadView; +class ApplyView; +class Sandbox; +class NetClock; + +/** Get AMM pool balances. + */ +std::pair +ammPoolHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue1, + Issue const& issue2, + FreezeHandling freezeHandling, + beast::Journal const j); + +/** Get AMM pool and LP token balances. If both optIssue are + * provided then they are used as the AMM token pair issues. + * Otherwise the missing issues are fetched from ammSle. + */ +Expected, TER> +ammHolds( + ReadView const& view, + SLE const& ammSle, + std::optional const& optIssue1, + std::optional const& optIssue2, + FreezeHandling freezeHandling, + beast::Journal const j); + +/** Get the balance of LP tokens. + */ +STAmount +ammLPHolds( + ReadView const& view, + Currency const& cur1, + Currency const& cur2, + AccountID const& ammAccount, + AccountID const& lpAccount, + beast::Journal const j); + +STAmount +ammLPHolds( + ReadView const& view, + SLE const& ammSle, + AccountID const& lpAccount, + beast::Journal const j); + +/** Get AMM trading fee for the given account. The fee is discounted + * if the account is the auction slot owner or one of the slot's authorized + * accounts. + */ +std::uint16_t +getTradingFee( + ReadView const& view, + SLE const& ammSle, + AccountID const& account); + +/** Returns total amount held by AMM for the given token. + */ +STAmount +ammAccountHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue); + +} // namespace ripple + +#endif // RIPPLE_APP_MISC_AMMUTILS_H_INLCUDED diff --git a/src/ripple/app/misc/impl/AMMHelpers.cpp b/src/ripple/app/misc/impl/AMMHelpers.cpp new file mode 100644 index 00000000000..736743eaaf7 --- /dev/null +++ b/src/ripple/app/misc/impl/AMMHelpers.cpp @@ -0,0 +1,206 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include + +namespace ripple { + +STAmount +ammLPTokens( + STAmount const& asset1, + STAmount const& asset2, + Issue const& lptIssue) +{ + auto const tokens = root2(asset1 * asset2); + return toSTAmount(lptIssue, tokens); +} + +/* + * Equation 3: + * t = T * [(b/B - (sqrt(f2**2 - b/(B*f1)) - f2)) / + * (1 + sqrt(f2**2 - b/(B*f1)) - f2)] + * where f1 = 1 - tfee, f2 = (1 - tfee/2)/f1 + */ +STAmount +lpTokensIn( + STAmount const& asset1Balance, + STAmount const& asset1Deposit, + STAmount const& lptAMMBalance, + std::uint16_t tfee) +{ + auto const f1 = feeMult(tfee); + auto const f2 = feeMultHalf(tfee) / f1; + Number const r = asset1Deposit / asset1Balance; + auto const c = root2(f2 * f2 + r / f1) - f2; + auto const t = lptAMMBalance * (r - c) / (1 + c); + return toSTAmount(lptAMMBalance.issue(), t); +} + +/* Equation 4 solves equation 3 for b: + * Let f1 = 1 - tfee, f2 = (1 - tfee/2)/f1, t1 = t/T, t2 = 1 + t1, R = b/B + * then + * t1 = [R - sqrt(f2**2 + R/f1) + f2] / [1 + sqrt(f2**2 + R/f1] - f2] => + * sqrt(f2**2 + R/f1)*(t1 + 1) = R + f2 + t1*f2 - t1 => + * sqrt(f2**2 + R/f1)*t2 = R + t2*f2 - t1 => + * sqrt(f2**2 + R/f1) = R/t2 + f2 - t1/t2, let d = f2 - t1/t2 => + * sqrt(f2**2 + R/f1) = R/t2 + d => + * f2**2 + R/f1 = (R/t2)**2 +2*d*R/t2 + d**2 => + * (R/t2)**2 + R*(2*d/t2 - 1/f1) + d**2 - f2**2 = 0 + */ +STAmount +ammAssetIn( + STAmount const& asset1Balance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee) +{ + auto const f1 = feeMult(tfee); + auto const f2 = feeMultHalf(tfee) / f1; + auto const t1 = lpTokens / lptAMMBalance; + auto const t2 = 1 + t1; + auto const d = f2 - t1 / t2; + auto const a = 1 / (t2 * t2); + auto const b = 2 * d / t2 - 1 / f1; + auto const c = d * d - f2 * f2; + return toSTAmount( + asset1Balance.issue(), asset1Balance * solveQuadraticEq(a, b, c)); +} + +/* Equation 7: + * t = T * (c - sqrt(c**2 - 4*R))/2 + * where R = b/B, c = R*fee + 2 - fee + */ +STAmount +lpTokensOut( + STAmount const& asset1Balance, + STAmount const& asset1Withdraw, + STAmount const& lptAMMBalance, + std::uint16_t tfee) +{ + Number const fr = asset1Withdraw / asset1Balance; + auto const f1 = getFee(tfee); + auto const c = fr * f1 + 2 - f1; + auto const t = lptAMMBalance * (c - root2(c * c - 4 * fr)) / 2; + return toSTAmount(lptAMMBalance.issue(), t); +} + +/* Equation 8 solves equation 7 for b: + * c - 2*t/T = sqrt(c**2 - 4*R) => + * c**2 - 4*c*t/T + 4*t**2/T**2 = c**2 - 4*R => + * -4*c*t/T + 4*t**2/T**2 = -4*R => + * -c*t/T + t**2/T**2 = -R -=> + * substitute c = R*f + 2 - f => + * -(t/T)*(R*f + 2 - f) + (t/T)**2 = -R, let t1 = t/T => + * -t1*R*f -2*t1 +t1*f +t1**2 = -R => + * R = (t1**2 + t1*(f - 2)) / (t1*f - 1) + */ +STAmount +withdrawByTokens( + STAmount const& assetBalance, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee) +{ + auto const f = getFee(tfee); + Number const t1 = lpTokens / lptAMMBalance; + auto const b = assetBalance * (t1 * t1 - t1 * (2 - f)) / (t1 * f - 1); + return toSTAmount(assetBalance.issue(), b); +} + +Number +square(Number const& n) +{ + return n * n; +} + +STAmount +adjustLPTokens( + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + bool isDeposit) +{ + // Force rounding downward to ensure adjusted tokens are less or equal + // to requested tokens. + saveNumberRoundMode rm(Number::setround(Number::rounding_mode::downward)); + if (isDeposit) + return (lptAMMBalance + lpTokens) - lptAMMBalance; + return (lpTokens - lptAMMBalance) + lptAMMBalance; +} + +std::tuple, STAmount> +adjustAmountsByLPTokens( + STAmount const& amountBalance, + STAmount const& amount, + std::optional const& amount2, + STAmount const& lptAMMBalance, + STAmount const& lpTokens, + std::uint16_t tfee, + bool isDeposit) +{ + auto const lpTokensActual = + adjustLPTokens(lptAMMBalance, lpTokens, isDeposit); + + if (lpTokensActual == beast::zero) + { + auto const amount2Opt = + amount2 ? std::make_optional(STAmount{}) : std::nullopt; + return std::make_tuple(STAmount{}, amount2Opt, lpTokensActual); + } + + if (lpTokensActual < lpTokens) + { + // Equal trade + if (amount2) + { + Number const fr = lpTokensActual / lpTokens; + auto const amountActual = toSTAmount(amount.issue(), fr * amount); + auto const amount2Actual = + toSTAmount(amount2->issue(), fr * *amount2); + return std::make_tuple( + amountActual < amount ? amountActual : amount, + amount2Actual < amount2 ? amount2Actual : amount2, + lpTokensActual); + } + + // Single trade + auto const amountActual = [&]() { + if (isDeposit) + return ammAssetIn( + amountBalance, lptAMMBalance, lpTokensActual, tfee); + else + return withdrawByTokens( + amountBalance, lptAMMBalance, lpTokens, tfee); + }(); + return amountActual < amount + ? std::make_tuple(amountActual, std::nullopt, lpTokensActual) + : std::make_tuple(amount, std::nullopt, lpTokensActual); + } + + assert(lpTokensActual == lpTokens); + + return {amount, amount2, lpTokensActual}; +} + +Number +solveQuadraticEq(Number const& a, Number const& b, Number const& c) +{ + return (-b + root2(b * b - 4 * a * c)) / (2 * a); +} + +} // namespace ripple diff --git a/src/ripple/app/misc/impl/AMMUtils.cpp b/src/ripple/app/misc/impl/AMMUtils.cpp new file mode 100644 index 00000000000..7156c77f21a --- /dev/null +++ b/src/ripple/app/misc/impl/AMMUtils.cpp @@ -0,0 +1,191 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include +#include +#include +#include +#include +#include + +namespace ripple { + +std::pair +ammPoolHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue1, + Issue const& issue2, + FreezeHandling freezeHandling, + beast::Journal const j) +{ + auto const assetInBalance = + accountHolds(view, ammAccountID, issue1, freezeHandling, j); + auto const assetOutBalance = + accountHolds(view, ammAccountID, issue2, freezeHandling, j); + return std::make_pair(assetInBalance, assetOutBalance); +} + +Expected, TER> +ammHolds( + ReadView const& view, + SLE const& ammSle, + std::optional const& optIssue1, + std::optional const& optIssue2, + FreezeHandling freezeHandling, + beast::Journal const j) +{ + auto const issues = [&]() -> std::optional> { + auto const issue1 = ammSle[sfAsset]; + auto const issue2 = ammSle[sfAsset2]; + if (optIssue1 && optIssue2) + { + if (invalidAMMAssetPair( + *optIssue1, + *optIssue2, + std::make_optional(std::make_pair(issue1, issue2)))) + { + JLOG(j.debug()) << "ammHolds: Invalid optIssue1 or optIssue2 " + << *optIssue1 << " " << *optIssue2; + return std::nullopt; + } + return std::make_optional(std::make_pair(*optIssue1, *optIssue2)); + } + auto const singleIssue = + [&issue1, &issue2, &j]( + Issue checkIssue, + const char* label) -> std::optional> { + if (checkIssue == issue1) + return std::make_optional(std::make_pair(issue1, issue2)); + else if (checkIssue == issue2) + return std::make_optional(std::make_pair(issue2, issue1)); + JLOG(j.debug()) + << "ammHolds: Invalid " << label << " " << checkIssue; + return std::nullopt; + }; + if (optIssue1) + { + return singleIssue(*optIssue1, "optIssue1"); + } + else if (optIssue2) + { + return singleIssue(*optIssue2, "optIssue2"); + } + return std::make_optional(std::make_pair(issue1, issue2)); + }(); + if (!issues) + return Unexpected(tecAMM_INVALID_TOKENS); + auto const [asset1, asset2] = ammPoolHolds( + view, + ammSle.getAccountID(sfAccount), + issues->first, + issues->second, + freezeHandling, + j); + return std::make_tuple(asset1, asset2, ammSle[sfLPTokenBalance]); +} + +STAmount +ammLPHolds( + ReadView const& view, + Currency const& cur1, + Currency const& cur2, + AccountID const& ammAccount, + AccountID const& lpAccount, + beast::Journal const j) +{ + return accountHolds( + view, + lpAccount, + ammLPTCurrency(cur1, cur2), + ammAccount, + FreezeHandling::fhZERO_IF_FROZEN, + j); +} + +STAmount +ammLPHolds( + ReadView const& view, + SLE const& ammSle, + AccountID const& lpAccount, + beast::Journal const j) +{ + return ammLPHolds( + view, + ammSle[sfAsset].currency, + ammSle[sfAsset2].currency, + ammSle[sfAccount], + lpAccount, + j); +} + +std::uint16_t +getTradingFee(ReadView const& view, SLE const& ammSle, AccountID const& account) +{ + using namespace std::chrono; + if (ammSle.isFieldPresent(sfAuctionSlot)) + { + auto const& auctionSlot = + static_cast(ammSle.peekAtField(sfAuctionSlot)); + // Not expired + if (auto const expiration = auctionSlot[~sfExpiration]; + duration_cast( + view.info().parentCloseTime.time_since_epoch()) + .count() < expiration) + { + if (auctionSlot[~sfAccount] == account) + return auctionSlot[sfDiscountedFee]; + if (auctionSlot.isFieldPresent(sfAuthAccounts)) + { + for (auto const& acct : + auctionSlot.getFieldArray(sfAuthAccounts)) + if (acct[~sfAccount] == account) + return auctionSlot[sfDiscountedFee]; + } + } + } + return ammSle[sfTradingFee]; +} + +STAmount +ammAccountHolds( + ReadView const& view, + AccountID const& ammAccountID, + Issue const& issue) +{ + if (isXRP(issue)) + { + if (auto const sle = view.read(keylet::account(ammAccountID))) + return (*sle)[sfBalance]; + } + else if (auto const sle = view.read( + keylet::line(ammAccountID, issue.account, issue.currency)); + sle && + !isFrozen(view, ammAccountID, issue.currency, issue.account)) + { + auto amount = (*sle)[sfBalance]; + if (ammAccountID > issue.account) + amount.negate(); + amount.setIssuer(issue.account); + return amount; + } + + return STAmount{issue}; +} + +} // namespace ripple diff --git a/src/ripple/app/paths/AMMContext.h b/src/ripple/app/paths/AMMContext.h new file mode 100644 index 00000000000..06835189bb7 --- /dev/null +++ b/src/ripple/app/paths/AMMContext.h @@ -0,0 +1,119 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_PATHS_AMMCONTEXT_H_INCLUDED +#define RIPPLE_APP_PATHS_AMMCONTEXT_H_INCLUDED + +#include + +#include + +namespace ripple { + +/** Maintains AMM info per overall payment engine execution and + * individual iteration. + * Only one instance of this class is created in Flow.cpp::flow(). + * The reference is percolated through calls to AMMLiquidity class, + * which handles AMM offer generation. + */ +class AMMContext +{ +public: + // Restrict number of AMM offers. If this restriction is removed + // then need to restrict in some other way because AMM offers are + // not counted in the BookStep offer counter. + constexpr static std::uint8_t MaxIterations = 30; + +private: + // Tx account owner is required to get the AMM trading fee in BookStep + AccountID account_; + // true if payment has multiple paths + bool multiPath_{false}; + // Is true if AMM offer is consumed during a payment engine iteration. + bool ammUsed_{false}; + // Counter of payment engine iterations with consumed AMM + std::uint16_t ammIters_{0}; + +public: + AMMContext(AccountID const& account, bool multiPath) + : account_(account), multiPath_(multiPath) + { + } + ~AMMContext() = default; + AMMContext(AMMContext const&) = delete; + AMMContext& + operator=(AMMContext const&) = delete; + + bool + multiPath() const + { + return multiPath_; + } + + void + setMultiPath(bool fs) + { + multiPath_ = fs; + } + + void + setAMMUsed() + { + ammUsed_ = true; + } + + void + update() + { + if (ammUsed_) + ++ammIters_; + ammUsed_ = false; + } + + bool + maxItersReached() const + { + return ammIters_ >= MaxIterations; + } + + std::uint16_t + curIters() const + { + return ammIters_; + } + + AccountID + account() const + { + return account_; + } + + /** Strand execution may fail. Reset the flag at the start + * of each payment engine iteration. + */ + void + clear() + { + ammUsed_ = false; + } +}; + +} // namespace ripple + +#endif // RIPPLE_APP_PATHS_AMMCONTEXT_H_INCLUDED diff --git a/src/ripple/app/paths/AMMLiquidity.h b/src/ripple/app/paths/AMMLiquidity.h new file mode 100644 index 00000000000..7757bd4684d --- /dev/null +++ b/src/ripple/app/paths/AMMLiquidity.h @@ -0,0 +1,148 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_TX_AMMLIQUIDITY_H_INCLUDED +#define RIPPLE_APP_TX_AMMLIQUIDITY_H_INCLUDED + +#include "ripple/app/misc/AMMHelpers.h" +#include "ripple/app/misc/AMMUtils.h" +#include "ripple/app/paths/AMMContext.h" +#include "ripple/basics/Log.h" +#include "ripple/ledger/ReadView.h" +#include "ripple/ledger/View.h" +#include "ripple/protocol/Quality.h" +#include "ripple/protocol/STLedgerEntry.h" + +namespace ripple { + +template +class AMMOffer; + +/** AMMLiquidity class provides AMM offers to BookStep class. + * The offers are generated in two ways. If there are multiple + * paths specified to the payment transaction then the offers + * are generated based on the Fibonacci sequence with + * a limited number of payment engine iterations consuming AMM offers. + * These offers behave the same way as CLOB offers in that if + * there is a limiting step, then the offers are adjusted + * based on their quality. + * If there is only one path specified in the payment transaction + * then the offers are generated based on the competing CLOB offer + * quality. In this case the offer's size is set in such a way + * that the new AMM's pool spot price quality is equal to the CLOB's + * offer quality. + */ +template +class AMMLiquidity +{ +private: + inline static const Number InitialFibSeqPct = Number(5) / 20000; + AMMContext& ammContext_; + AccountID const ammAccountID_; + std::uint32_t const tradingFee_; + Issue const issueIn_; + Issue const issueOut_; + // Initial AMM pool balances + TAmounts const initialBalances_; + beast::Journal const j_; + +public: + AMMLiquidity( + ReadView const& view, + AccountID const& ammAccountID, + std::uint32_t tradingFee, + Issue const& in, + Issue const& out, + AMMContext& ammContext, + beast::Journal j); + ~AMMLiquidity() = default; + AMMLiquidity(AMMLiquidity const&) = delete; + AMMLiquidity& + operator=(AMMLiquidity const&) = delete; + + /** Generate AMM offer. Returns nullopt if clobQuality is provided + * and it is better than AMM offer quality. Otherwise returns AMM offer. + * If clobQuality is provided then AMM offer size is set based on the + * quality. + */ + std::optional> + getOffer(ReadView const& view, std::optional const& clobQuality) + const; + + AccountID const& + ammAccount() const + { + return ammAccountID_; + } + + bool + multiPath() const + { + return ammContext_.multiPath(); + } + + std::uint32_t + tradingFee() const + { + return tradingFee_; + } + + AMMContext& + context() const + { + return ammContext_; + } + + Issue const& + issueIn() const + { + return issueIn_; + } + + Issue const& + issueOut() const + { + return issueOut_; + } + +private: + /** Fetches current AMM balances. + */ + TAmounts + fetchBalances(ReadView const& view) const; + + /** Generate AMM offers with the offer size based on Fibonacci sequence. + * The sequence corresponds to the payment engine iterations with AMM + * liquidity. Iterations that don't consume AMM offers don't count. + * The number of iterations with AMM offers is limited. + * If the generated offer exceeds the pool balance then the function + * throws overflow exception. + */ + TAmounts + generateFibSeqOffer(TAmounts const& balances) const; + + /** Generate max offer + */ + AMMOffer + maxOffer(TAmounts const& balances) const; +}; + +} // namespace ripple + +#endif // RIPPLE_APP_TX_AMMLIQUIDITY_H_INCLUDED diff --git a/src/ripple/app/paths/AMMOffer.h b/src/ripple/app/paths/AMMOffer.h new file mode 100644 index 00000000000..10e6017dd96 --- /dev/null +++ b/src/ripple/app/paths/AMMOffer.h @@ -0,0 +1,149 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_APP_AMMOFFER_H_INCLUDED +#define RIPPLE_APP_AMMOFFER_H_INCLUDED + +#include +#include +#include +#include + +namespace ripple { + +template +class AMMLiquidity; +class QualityFunction; + +/** Represents synthetic AMM offer in BookStep. AMMOffer mirrors TOffer + * methods for use in generic BookStep methods. AMMOffer amounts + * are changed indirectly in BookStep limiting steps. + */ +template +class AMMOffer +{ +private: + AMMLiquidity const& ammLiquidity_; + // Initial offer amounts. It is fibonacci seq generated for multi-path. + // If the offer size is set based on the competing CLOB offer then + // the AMM offer size is such that if the offer is consumed then + // the updated AMM pool SP quality is going to be equal to competing + // CLOB offer quality. If there is no competing CLOB offer then + // the initial size is set to in=cMax[Native,Value],balances.out. + // While this is not a "real" offer it simulates the case of + // the swap out of the entire side of the pool, in which case + // the swap in amount is infinite. + TAmounts const amounts_; + // If seated then current pool balances. Used in one-path limiting steps + // to swap in/out. + std::optional> const balances_; + // The Spot Price quality if balances != amounts + // else the amounts quality + Quality const quality_; + // AMM offer can be consumed once at a given iteration + bool consumed_; + +public: + AMMOffer( + AMMLiquidity const& ammLiquidity, + TAmounts const& amounts, + std::optional> const& balances, + Quality const& quality); + + Quality + quality() const noexcept + { + return quality_; + } + + Issue const& + issueIn() const; + + Issue const& + issueOut() const; + + AccountID const& + owner() const; + + std::optional + key() const + { + return std::nullopt; + } + + TAmounts const& + amount() const; + + void + consume(ApplyView& view, TAmounts const& consumed); + + bool + fully_consumed() const + { + return consumed_; + } + + /** Limit out of the provided offer. If one-path then swapOut + * using current balances. If multi-path then ceil_out using + * current quality. + */ + TAmounts + limitOut( + TAmounts const& offrAmt, + TOut const& limit, + bool fixReducedOffers, + bool roundUp) const; + + /** Limit in of the provided offer. If one-path then swapIn + * using current balances. If multi-path then ceil_in using + * current quality. + */ + TAmounts + limitIn(TAmounts const& offrAmt, TIn const& limit) const; + + QualityFunction + getQualityFunc() const; + + /** Send funds without incurring the transfer fee + */ + template + static TER + send(Args&&... args) + { + return accountSend(std::forward(args)..., WaiveTransferFee::Yes); + } + + bool + isFunded() const + { + // AMM offer is fully funded by the pool + return true; + } + + static std::pair + adjustRates(std::uint32_t ofrInRate, std::uint32_t ofrOutRate) + { + // AMM doesn't pay transfer fee on Payment tx + return {ofrInRate, QUALITY_ONE}; + } +}; + +} // namespace ripple + +#endif // RIPPLE_APP_AMMOFFER_H_INCLUDED diff --git a/src/ripple/app/paths/Flow.cpp b/src/ripple/app/paths/Flow.cpp index f177cfc1116..3d060fdc6bd 100644 --- a/src/ripple/app/paths/Flow.cpp +++ b/src/ripple/app/paths/Flow.cpp @@ -17,6 +17,7 @@ */ //============================================================================== +#include #include #include #include @@ -84,6 +85,8 @@ flow( if (sendMax) sendMaxIssue = sendMax->issue(); + AMMContext ammContext(src, false); + // convert the paths to a collection of strands. Each strand is the // collection of account->account steps and book steps that may be used in // this payment. @@ -98,6 +101,7 @@ flow( defaultPaths, ownerPaysTransferFee, offerCrossing, + ammContext, j); if (toStrandsTer != tesSUCCESS) @@ -107,6 +111,8 @@ flow( return result; } + ammContext.setMultiPath(strands.size() > 1); + if (j.trace()) { j.trace() << "\nsrc: " << src << "\ndst: " << dst @@ -145,6 +151,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } @@ -163,6 +170,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } @@ -181,6 +189,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } @@ -198,6 +207,7 @@ flow( limitQuality, sendMax, j, + ammContext, flowDebugInfo)); } diff --git a/src/ripple/app/paths/impl/AMMLiquidity.cpp b/src/ripple/app/paths/impl/AMMLiquidity.cpp new file mode 100644 index 00000000000..3f22ebacec5 --- /dev/null +++ b/src/ripple/app/paths/impl/AMMLiquidity.cpp @@ -0,0 +1,223 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== +#include + +#include + +namespace ripple { + +template +AMMLiquidity::AMMLiquidity( + ReadView const& view, + AccountID const& ammAccountID, + std::uint32_t tradingFee, + Issue const& in, + Issue const& out, + AMMContext& ammContext, + beast::Journal j) + : ammContext_(ammContext) + , ammAccountID_(ammAccountID) + , tradingFee_(tradingFee) + , issueIn_(in) + , issueOut_(out) + , initialBalances_{fetchBalances(view)} + , j_(j) +{ +} + +template +TAmounts +AMMLiquidity::fetchBalances(ReadView const& view) const +{ + auto const assetIn = ammAccountHolds(view, ammAccountID_, issueIn_); + auto const assetOut = ammAccountHolds(view, ammAccountID_, issueOut_); + // This should not happen. + if (assetIn < beast::zero || assetOut < beast::zero) + Throw("AMMLiquidity: invalid balances"); + + return TAmounts{get(assetIn), get(assetOut)}; +} + +template +TAmounts +AMMLiquidity::generateFibSeqOffer( + TAmounts const& balances) const +{ + TAmounts cur{}; + + cur.in = toAmount( + getIssue(balances.in), + InitialFibSeqPct * initialBalances_.in, + Number::rounding_mode::upward); + cur.out = swapAssetIn(initialBalances_, cur.in, tradingFee_); + + if (ammContext_.curIters() == 0) + return cur; + + // clang-format off + constexpr std::uint32_t fib[AMMContext::MaxIterations] = { + 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, + 1597, 2584, 4181, 6765, 10946, 17711, 28657, 46368, 75025, 121393, + 196418, 317811, 514229, 832040, 1346269}; + // clang-format on + + assert(!ammContext_.maxItersReached()); + + cur.out = toAmount( + getIssue(balances.out), + cur.out * fib[ammContext_.curIters() - 1], + Number::rounding_mode::downward); + // swapAssetOut() returns negative in this case + if (cur.out >= balances.out) + Throw( + "AMMLiquidity: generateFibSeqOffer exceeds the balance"); + + cur.in = swapAssetOut(balances, cur.out, tradingFee_); + + return cur; +} + +template +constexpr T +maxAmount() +{ + if constexpr (std::is_same_v) + return XRPAmount(STAmount::cMaxNative); + else if constexpr (std::is_same_v) + return IOUAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); + else if constexpr (std::is_same_v) + return STAmount(STAmount::cMaxValue / 2, STAmount::cMaxOffset); +} + +template +AMMOffer +AMMLiquidity::maxOffer(TAmounts const& balances) const +{ + return AMMOffer( + *this, + {maxAmount(), + swapAssetIn(balances, maxAmount(), tradingFee_)}, + balances, + Quality{balances}); +} + +template +std::optional> +AMMLiquidity::getOffer( + ReadView const& view, + std::optional const& clobQuality) const +{ + // Can't generate more offers if multi-path. + if (ammContext_.maxItersReached()) + return std::nullopt; + + auto const balances = fetchBalances(view); + + // Frozen accounts + if (balances.in == beast::zero || balances.out == beast::zero) + { + JLOG(j_.debug()) << "AMMLiquidity::getOffer, frozen accounts"; + return std::nullopt; + } + + JLOG(j_.trace()) << "AMMLiquidity::getOffer balances " + << to_string(initialBalances_.in) << " " + << to_string(initialBalances_.out) << " new balances " + << to_string(balances.in) << " " + << to_string(balances.out); + + // Can't generate AMM with a better quality than CLOB's + // quality if AMM's Spot Price quality is less than CLOB quality or is + // within a threshold. + // Spot price quality (SPQ) is calculated within some precision threshold. + // On the next iteration, after SPQ is changed, the new SPQ might be close + // to the requested clobQuality but not exactly and potentially SPQ may keep + // on approaching clobQuality for many iterations. Checking for the quality + // threshold prevents this scenario. + if (auto const spotPriceQ = Quality{balances}; clobQuality && + (spotPriceQ <= clobQuality || + withinRelativeDistance(spotPriceQ, *clobQuality, Number(1, -7)))) + { + JLOG(j_.trace()) << "AMMLiquidity::getOffer, higher clob quality"; + return std::nullopt; + } + + auto offer = [&]() -> std::optional> { + try + { + if (ammContext_.multiPath()) + { + auto const amounts = generateFibSeqOffer(balances); + if (clobQuality && Quality{amounts} < clobQuality) + return std::nullopt; + return AMMOffer( + *this, amounts, std::nullopt, Quality{amounts}); + } + else if (!clobQuality) + { + // If there is no CLOB to compare against, return the largest + // amount, which doesn't overflow. The size is going to be + // changed in BookStep per either deliver amount limit, or + // sendmax, or available output or input funds. + return maxOffer(balances); + } + else if ( + auto const amounts = + changeSpotPriceQuality(balances, *clobQuality, tradingFee_)) + { + return AMMOffer( + *this, *amounts, balances, Quality{*amounts}); + } + } + catch (std::overflow_error const& e) + { + JLOG(j_.error()) << "AMMLiquidity::getOffer overflow " << e.what(); + return maxOffer(balances); + } + catch (std::exception const& e) + { + JLOG(j_.error()) << "AMMLiquidity::getOffer exception " << e.what(); + } + return std::nullopt; + }(); + + if (offer) + { + if (offer->amount().in > beast::zero && + offer->amount().out > beast::zero) + { + JLOG(j_.trace()) + << "AMMLiquidity::getOffer, created " + << to_string(offer->amount().in) << "/" << issueIn_ << " " + << to_string(offer->amount().out) << "/" << issueOut_; + return offer; + } + + JLOG(j_.error()) << "AMMLiquidity::getOffer, failed"; + } + + return std::nullopt; +} + +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; +template class AMMLiquidity; + +} // namespace ripple diff --git a/src/ripple/app/paths/impl/AMMOffer.cpp b/src/ripple/app/paths/impl/AMMOffer.cpp new file mode 100644 index 00000000000..10b75b78565 --- /dev/null +++ b/src/ripple/app/paths/impl/AMMOffer.cpp @@ -0,0 +1,143 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2023 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//==============================================================================/ +#include + +#include +#include + +namespace ripple { + +template +AMMOffer::AMMOffer( + AMMLiquidity const& ammLiquidity, + TAmounts const& amounts, + std::optional> const& balances, + Quality const& quality) + : ammLiquidity_(ammLiquidity) + , amounts_(amounts) + , balances_(balances) + , quality_(quality) + , consumed_(false) +{ +} + +template +Issue const& +AMMOffer::issueIn() const +{ + return ammLiquidity_.issueIn(); +} + +template +Issue const& +AMMOffer::issueOut() const +{ + return ammLiquidity_.issueOut(); +} + +template +AccountID const& +AMMOffer::owner() const +{ + return ammLiquidity_.ammAccount(); +} + +template +TAmounts const& +AMMOffer::amount() const +{ + return amounts_; +} + +template +void +AMMOffer::consume( + ApplyView& view, + TAmounts const& consumed) +{ + // Consumed offer must be less or equal to the original + if (consumed.in > amounts_.in || consumed.out > amounts_.out) + Throw("Invalid consumed AMM offer."); + // AMM pool is updated when the amounts are transferred + // in BookStep::consumeOffer(). + + consumed_ = true; + + // Let the context know AMM offer is consumed + ammLiquidity_.context().setAMMUsed(); +} + +template +TAmounts +AMMOffer::limitOut( + TAmounts const& offrAmt, + TOut const& limit, + bool fixReducedOffers, + bool roundUp) const +{ + // Change the offer size proportionally to the original offer quality + // to keep the strands quality order unchanged. The taker pays slightly + // more for the offer in this case, which results in a slightly higher + // pool product than the original pool product. I.e. if the original + // pool is poolPays, poolGets and the offer is assetIn, assetOut then + // poolPays * poolGets < (poolPays - assetOut) * (poolGets + assetIn) + if (ammLiquidity_.multiPath()) + { + if (fixReducedOffers) + // It turns out that the ceil_out implementation has some slop in + // it. ceil_out_strict removes that slop. But removing that slop + // affects transaction outcomes, so the change must be made using + // an amendment. + return quality().ceil_out_strict(offrAmt, limit, roundUp); + return quality().ceil_out(offrAmt, limit); + } + // Change the offer size according to the conservation function. The offer + // quality is increased in this case, but it doesn't matter since there is + // only one path. + return {swapAssetOut(*balances_, limit, ammLiquidity_.tradingFee()), limit}; +} + +template +TAmounts +AMMOffer::limitIn( + TAmounts const& offrAmt, + TIn const& limit) const +{ + // See the comments above in limitOut(). + if (ammLiquidity_.multiPath()) + return quality().ceil_in(offrAmt, limit); + return {limit, swapAssetIn(*balances_, limit, ammLiquidity_.tradingFee())}; +} + +template +QualityFunction +AMMOffer::getQualityFunc() const +{ + if (ammLiquidity_.multiPath()) + return QualityFunction{quality(), QualityFunction::CLOBLikeTag{}}; + return QualityFunction{ + *balances_, ammLiquidity_.tradingFee(), QualityFunction::AMMTag{}}; +} + +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; +template class AMMOffer; + +} // namespace ripple diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 555d90fac8c..e82acbde817 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -17,7 +17,9 @@ */ //============================================================================== -#include +#include +#include +#include #include #include #include @@ -42,6 +44,8 @@ template class BookStep : public StepImp> { protected: + enum class OfferType { AMM, CLOB }; + uint32_t const maxOffersToConsume_; Book book_; AccountID strandSrc_; @@ -59,6 +63,10 @@ class BookStep : public StepImp> be partially consumed multiple times during a payment. */ std::uint32_t offersUsed_ = 0; + // If set, AMM liquidity might be available + // if AMM offer quality is better than CLOB offer + // quality or there is no CLOB offer. + std::optional> ammLiquidity_; beast::Journal const j_; struct Cache @@ -91,6 +99,15 @@ class BookStep : public StepImp> , ownerPaysTransferFee_(ctx.ownerPaysTransferFee) , j_(ctx.j) { + if (auto const ammSle = ctx.view.read(keylet::amm(in, out))) + ammLiquidity_.emplace( + ctx.view, + (*ammSle)[sfAccount], + getTradingFee(ctx.view, *ammSle, ctx.ammContext.account()), + in, + out, + ctx.ammContext, + ctx.j); } Book const& @@ -132,6 +149,9 @@ class BookStep : public StepImp> qualityUpperBound(ReadView const& v, DebtDirection prevStepDir) const override; + std::pair, DebtDirection> + getQualityFunc(ReadView const& v, DebtDirection prevStepDir) const override; + std::uint32_t offersUsed() const override; @@ -205,13 +225,36 @@ class BookStep : public StepImp> DebtDirection prevStepDebtDir, Callback& callback) const; + // Offer is either TOffer or AMMOffer + template