diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b27a7dfd9bb..75616eb6b4e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -197,16 +197,39 @@ existing maintainer without a vote. ## Current Maintainers +Maintainers are users with admin access to the repo. Maintainers do not typically approve or deny pull requests. + +* [intelliot](https://github.com/intelliot) (Ripple) * [JoelKatz](https://github.com/JoelKatz) (Ripple) -* [manojsdoshi](https://github.com/manojsdoshi) (Ripple) -* [n3tc4t](https://github.com/n3tc4t) (XRPL Labs) -* [Nik Bougalis](https://github.com/nbougalis) * [nixer89](https://github.com/nixer89) (XRP Ledger Foundation) -* [RichardAH](https://github.com/RichardAH) (XRPL Labs + XRP Ledger Foundation) -* [seelabs](https://github.com/seelabs) (Ripple) * [Silkjaer](https://github.com/Silkjaer) (XRP Ledger Foundation) * [WietseWind](https://github.com/WietseWind) (XRPL Labs + XRP Ledger Foundation) + +## Current Code Reviewers + +Code Reviewers are developers who have the ability to review and approve source code changes. + +* [HowardHinnant](https://github.com/HowardHinnant) (Ripple) +* [scottschurr](https://github.com/scottschurr) (Ripple) +* [seelabs](https://github.com/seelabs) (Ripple) * [Ed Hennis](https://github.com/ximinez) (Ripple) +* [mvadari](https://github.com/mvadari) (Ripple) +* [thejohnfreeman](https://github.com/thejohnfreeman) (Ripple) +* [Bronek](https://github.com/Bronek) (Ripple) +* [manojsdoshi](https://github.com/manojsdoshi) (Ripple) +* [godexsoft](https://github.com/godexsoft) (Ripple) +* [mDuo13](https://github.com/mDuo13) (Ripple) +* [ckniffen](https://github.com/ckniffen) (Ripple) +* [arihantkothari](https://github.com/arihantkothari) (Ripple) +* [pwang200](https://github.com/pwang200) (Ripple) +* [sophiax851](https://github.com/sophiax851) (Ripple) +* [shawnxie999](https://github.com/shawnxie999) (Ripple) +* [gregtatcam](https://github.com/gregtatcam) (Ripple) +* [mtrippled](https://github.com/mtrippled) (Ripple) +* [ckeshava](https://github.com/ckeshava) (Ripple) +* [nbougalis](https://github.com/nbougalis) None +* [RichardAH](https://github.com/RichardAH) (XRPL Labs + XRP Ledger Foundation) +* [dangell7](https://github.com/dangell7) (XRPL Labs) [1]: https://docs.github.com/en/get-started/quickstart/contributing-to-projects diff --git a/conanfile.py b/conanfile.py index 1d4259777cf..e61728039fc 100644 --- a/conanfile.py +++ b/conanfile.py @@ -116,7 +116,7 @@ def requirements(self): self.requires('rocksdb/6.29.5') exports_sources = ( - 'CMakeLists.txt', 'Builds/*', 'bin/getRippledInfo', 'src/*', 'cfg/*' + 'CMakeLists.txt', 'Builds/*', 'bin/getRippledInfo', 'src/*', 'cfg/*', 'external/*' ) def layout(self): diff --git a/src/ripple/app/misc/AMMHelpers.h b/src/ripple/app/misc/AMMHelpers.h index 7bdaf23d69f..787bb2300a3 100644 --- a/src/ripple/app/misc/AMMHelpers.h +++ b/src/ripple/app/misc/AMMHelpers.h @@ -21,7 +21,9 @@ #define RIPPLE_APP_MISC_AMMHELPERS_H_INCLUDED #include +#include #include +#include #include #include #include @@ -35,6 +37,20 @@ namespace ripple { +namespace detail { + +Number +reduceOffer(auto const& amount) +{ + static Number const reducedOfferPct(9999, -4); + + // Make sure the result is always less than amount or zero. + NumberRoundModeGuard mg(Number::towards_zero); + return amount * reducedOfferPct; +} + +} // namespace detail + /** 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 @@ -147,12 +163,165 @@ withinRelativeDistance(Amt const& calc, Amt const& req, Number const& 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(). +/** Solve quadratic equation to find takerGets or takerPays. Round + * to minimize the amount in order to maximize the quality. + */ +std::optional +solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c); + +/** Generate AMM offer starting with takerGets when AMM pool + * from the payment perspective is IOU(in)/XRP(out) + * Equations: + * Spot Price Quality after the offer is consumed: + * Qsp = (O - o) / (I + i) -- equation (1) + * where O is poolPays, I is poolGets, o is takerGets, i is takerPays + * Swap out: + * i = (I * o) / (O - o) * f -- equation (2) + * where f is (1 - tfee/100000), tfee is in basis points + * Effective price targetQuality: + * Qep = o / i -- equation (3) + * There are two scenarios to consider + * A) Qsp = Qep. Substitute i in (1) with (2) and solve for o + * and Qsp = targetQuality(Qt): + * o**2 + o * (I * Qt * (1 - 1 / f) - 2 * O) + O**2 - Qt * I * O = 0 + * B) Qep = Qsp. Substitute i in (3) with (2) and solve for o + * and Qep = targetQuality(Qt): + * o = O - I * Qt / f + * Since the scenario is not known a priori, both A and B are solved and + * the lowest value of o is takerGets. takerPays is calculated with + * swap out eq (2). If o is less or equal to 0 then the offer can't + * be generated. + */ +template +std::optional> +getAMMOfferStartWithTakerGets( + TAmounts const& pool, + Quality const& targetQuality, + std::uint16_t const& tfee) +{ + if (targetQuality.rate() == beast::zero) + return std::nullopt; + + NumberRoundModeGuard mg(Number::to_nearest); + auto const f = feeMult(tfee); + auto const a = 1; + auto const b = pool.in * (1 - 1 / f) / targetQuality.rate() - 2 * pool.out; + auto const c = + pool.out * pool.out - (pool.in * pool.out) / targetQuality.rate(); + + auto nTakerGets = solveQuadraticEqSmallest(a, b, c); + if (!nTakerGets || *nTakerGets <= 0) + return std::nullopt; // LCOV_EXCL_LINE + + auto const nTakerGetsConstraint = + pool.out - pool.in / (targetQuality.rate() * f); + if (nTakerGetsConstraint <= 0) + return std::nullopt; + + // Select the smallest to maximize the quality + if (nTakerGetsConstraint < *nTakerGets) + nTakerGets = nTakerGetsConstraint; + + auto getAmounts = [&pool, &tfee](Number const& nTakerGetsProposed) { + // Round downward to minimize the offer and to maximize the quality. + // This has the most impact when takerGets is XRP. + auto const takerGets = toAmount( + getIssue(pool.out), nTakerGetsProposed, Number::downward); + return TAmounts{ + swapAssetOut(pool, takerGets, tfee), takerGets}; + }; + + // Try to reduce the offer size to improve the quality. + // The quality might still not match the targetQuality for a tiny offer. + if (auto const amounts = getAmounts(*nTakerGets); + Quality{amounts} < targetQuality) + return getAmounts(detail::reduceOffer(amounts.out)); + else + return amounts; +} + +/** Generate AMM offer starting with takerPays when AMM pool + * from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out). + * Equations: + * Spot Price Quality after the offer is consumed: + * Qsp = (O - o) / (I + i) -- equation (1) + * where O is poolPays, I is poolGets, o is takerGets, i is takerPays + * Swap in: + * o = (O * i * f) / (I + i * f) -- equation (2) + * where f is (1 - tfee/100000), tfee is in basis points + * Effective price quality: + * Qep = o / i -- equation (3) + * There are two scenarios to consider + * A) Qsp = Qep. Substitute o in (1) with (2) and solve for i + * and Qsp = targetQuality(Qt): + * i**2 * f + i * I * (1 + f) + I**2 - I * O / Qt = 0 + * B) Qep = Qsp. Substitute i in (3) with (2) and solve for i + * and Qep = targetQuality(Qt): + * i = O / Qt - I / f + * Since the scenario is not known a priori, both A and B are solved and + * the lowest value of i is takerPays. takerGets is calculated with + * swap in eq (2). If i is less or equal to 0 then the offer can't + * be generated. + */ +template +std::optional> +getAMMOfferStartWithTakerPays( + TAmounts const& pool, + Quality const& targetQuality, + std::uint16_t tfee) +{ + if (targetQuality.rate() == beast::zero) + return std::nullopt; + + NumberRoundModeGuard mg(Number::to_nearest); + auto const f = feeMult(tfee); + auto const& a = f; + auto const b = pool.in * (1 + f); + auto const c = + pool.in * pool.in - pool.in * pool.out * targetQuality.rate(); + + auto nTakerPays = solveQuadraticEqSmallest(a, b, c); + if (!nTakerPays || nTakerPays <= 0) + return std::nullopt; // LCOV_EXCL_LINE + + auto const nTakerPaysConstraint = + pool.out * targetQuality.rate() - pool.in / f; + if (nTakerPaysConstraint <= 0) + return std::nullopt; + + // Select the smallest to maximize the quality + if (nTakerPaysConstraint < *nTakerPays) + nTakerPays = nTakerPaysConstraint; + + auto getAmounts = [&pool, &tfee](Number const& nTakerPaysProposed) { + // Round downward to minimize the offer and to maximize the quality. + // This has the most impact when takerPays is XRP. + auto const takerPays = toAmount( + getIssue(pool.in), nTakerPaysProposed, Number::downward); + return TAmounts{ + takerPays, swapAssetIn(pool, takerPays, tfee)}; + }; + + // Try to reduce the offer size to improve the quality. + // The quality might still not match the targetQuality for a tiny offer. + if (auto const amounts = getAmounts(*nTakerPays); + Quality{amounts} < targetQuality) + return getAmounts(detail::reduceOffer(amounts.in)); + else + return amounts; +} + +/** Generate AMM offer so that either updated Spot Price Quality (SPQ) + * is equal to LOB quality (in this case AMM offer quality is + * better than LOB quality) or AMM offer is equal to LOB quality + * (in this case SPQ is better than LOB quality). + * Pre-amendment code calculates takerPays first. If takerGets is XRP, + * it is rounded down, which results in worse offer quality than + * LOB quality, and the offer might fail to generate. + * Post-amendment code calculates the XRP offer side first. The result + * is rounded down, which makes the offer quality better. + * It might not be possible to match either SPQ or AMM offer to LOB + * quality. This generally happens at higher fees. * @param pool AMM pool balances * @param quality requested quality * @param tfee trading fee in basis points @@ -163,43 +332,111 @@ std::optional> changeSpotPriceQuality( TAmounts const& pool, Quality const& quality, - std::uint16_t tfee) + std::uint16_t tfee, + Rules const& rules, + beast::Journal j) { - 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) + if (!rules.enabled(fixAMMv1_1)) + { + // 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(). + 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; // LCOV_EXCL_LINE + 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) + { + JLOG(j.trace()) + << "changeSpotPriceQuality calc failed: " + << to_string(pool.in) << " " << to_string(pool.out) << " " + << quality << " " << tfee; + return std::nullopt; + } + auto const takerPays = + toAmount(getIssue(pool.in), nTakerPays, Number::upward); + // should not fail + if (auto const amounts = + TAmounts{ + takerPays, swapAssetIn(pool, takerPays, tfee)}; + Quality{amounts} < quality && + !withinRelativeDistance( + Quality{amounts}, quality, Number(1, -7))) + { + JLOG(j.error()) + << "changeSpotPriceQuality failed: " << to_string(pool.in) + << " " << to_string(pool.out) << " " + << " " << quality << " " << tfee << " " + << to_string(amounts.in) << " " << to_string(amounts.out); + Throw("changeSpotPriceQuality failed"); + } + else + { + JLOG(j.trace()) + << "changeSpotPriceQuality succeeded: " + << to_string(pool.in) << " " << to_string(pool.out) << " " + << " " << quality << " " << tfee << " " + << to_string(amounts.in) << " " << to_string(amounts.out); + return amounts; + } + } + JLOG(j.trace()) << "changeSpotPriceQuality calc failed: " + << to_string(pool.in) << " " << to_string(pool.out) + << " " << quality << " " << tfee; return std::nullopt; - else if (auto const nTakerPaysPropose = (-b + root2(res)) / (2 * a); - nTakerPaysPropose > 0) + } + + // Generate the offer starting with XRP side. Return seated offer amounts + // if the offer can be generated, otherwise nullopt. + auto const amounts = [&]() { + if (isXRP(getIssue(pool.out))) + return getAMMOfferStartWithTakerGets(pool, quality, tfee); + return getAMMOfferStartWithTakerPays(pool, quality, tfee); + }(); + if (!amounts) { - 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; + JLOG(j.trace()) << "changeSpotPrice calc failed: " << to_string(pool.in) + << " " << to_string(pool.out) << " " << quality << " " + << tfee << std::endl; + return std::nullopt; } - return std::nullopt; + + if (Quality{*amounts} < quality) + { + JLOG(j.error()) << "changeSpotPriceQuality failed: " + << to_string(pool.in) << " " << to_string(pool.out) + << " " << quality << " " << tfee << " " + << to_string(amounts->in) << " " + << to_string(amounts->out); + return std::nullopt; + } + + JLOG(j.trace()) << "changeSpotPriceQuality succeeded: " + << to_string(pool.in) << " " << to_string(pool.out) << " " + << " " << quality << " " << tfee << " " + << to_string(amounts->in) << " " << to_string(amounts->out); + + return amounts; } /** AMM pool invariant - the product (A * B) after swap in/out has to remain @@ -231,7 +468,7 @@ swapAssetIn( std::uint16_t tfee) { if (auto const& rules = getCurrentTransactionRules(); - rules && rules->enabled(fixAMMRounding)) + rules && rules->enabled(fixAMMv1_1)) { // set rounding to always favor the amm. Clip to zero. // calculate: @@ -275,8 +512,7 @@ swapAssetIn( if (swapOut.signum() < 0) return toAmount(getIssue(pool.out), 0); - return toAmount( - getIssue(pool.out), swapOut, Number::rounding_mode::downward); + return toAmount(getIssue(pool.out), swapOut, Number::downward); } else { @@ -284,7 +520,7 @@ swapAssetIn( getIssue(pool.out), pool.out - (pool.in * pool.out) / (pool.in + assetIn * feeMult(tfee)), - Number::rounding_mode::downward); + Number::downward); } } @@ -305,7 +541,7 @@ swapAssetOut( std::uint16_t tfee) { if (auto const& rules = getCurrentTransactionRules(); - rules && rules->enabled(fixAMMRounding)) + rules && rules->enabled(fixAMMv1_1)) { // set rounding to always favor the amm. Clip to zero. // calculate: @@ -349,8 +585,7 @@ swapAssetOut( if (swapIn.signum() < 0) return toAmount(getIssue(pool.in), 0); - return toAmount( - getIssue(pool.in), swapIn, Number::rounding_mode::upward); + return toAmount(getIssue(pool.in), swapIn, Number::upward); } else { @@ -358,7 +593,7 @@ swapAssetOut( getIssue(pool.in), ((pool.in * pool.out) / (pool.out - assetOut) - pool.in) / feeMult(tfee), - Number::rounding_mode::upward); + Number::upward); } } diff --git a/src/ripple/app/misc/AMMUtils.h b/src/ripple/app/misc/AMMUtils.h index c25503ceb9c..9cd4b7d6fec 100644 --- a/src/ripple/app/misc/AMMUtils.h +++ b/src/ripple/app/misc/AMMUtils.h @@ -113,6 +113,16 @@ initializeFeeAuctionVote( Issue const& lptIssue, std::uint16_t tfee); +/** Return true if the Liquidity Provider is the only AMM provider, false + * otherwise. Return tecINTERNAL if encountered an unexpected condition, + * for instance Liquidity Provider has more than one LPToken trustline. + */ +Expected +isOnlyLiquidityProvider( + ReadView const& view, + Issue const& ammIssue, + AccountID const& lpAccount); + } // 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 index 736743eaaf7..5ad59ea1c28 100644 --- a/src/ripple/app/misc/impl/AMMHelpers.cpp +++ b/src/ripple/app/misc/impl/AMMHelpers.cpp @@ -165,6 +165,13 @@ adjustAmountsByLPTokens( if (lpTokensActual < lpTokens) { + bool const ammRoundingEnabled = [&]() { + if (auto const& rules = getCurrentTransactionRules(); + rules && rules->enabled(fixAMMv1_1)) + return true; + return false; + }(); + // Equal trade if (amount2) { @@ -172,10 +179,14 @@ adjustAmountsByLPTokens( 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); + if (!ammRoundingEnabled) + return std::make_tuple( + amountActual < amount ? amountActual : amount, + amount2Actual < amount2 ? amount2Actual : amount2, + lpTokensActual); + else + return std::make_tuple( + amountActual, amount2Actual, lpTokensActual); } // Single trade @@ -183,13 +194,19 @@ adjustAmountsByLPTokens( if (isDeposit) return ammAssetIn( amountBalance, lptAMMBalance, lpTokensActual, tfee); - else + else if (!ammRoundingEnabled) return withdrawByTokens( amountBalance, lptAMMBalance, lpTokens, tfee); + else + return withdrawByTokens( + amountBalance, lptAMMBalance, lpTokensActual, tfee); }(); - return amountActual < amount - ? std::make_tuple(amountActual, std::nullopt, lpTokensActual) - : std::make_tuple(amount, std::nullopt, lpTokensActual); + if (!ammRoundingEnabled) + return amountActual < amount + ? std::make_tuple(amountActual, std::nullopt, lpTokensActual) + : std::make_tuple(amount, std::nullopt, lpTokensActual); + else + return std::make_tuple(amountActual, std::nullopt, lpTokensActual); } assert(lpTokensActual == lpTokens); @@ -203,4 +220,19 @@ solveQuadraticEq(Number const& a, Number const& b, Number const& c) return (-b + root2(b * b - 4 * a * c)) / (2 * a); } +// Minimize takerGets or takerPays +std::optional +solveQuadraticEqSmallest(Number const& a, Number const& b, Number const& c) +{ + auto const d = b * b - 4 * a * c; + if (d < 0) + return std::nullopt; + // use numerically stable citardauq formula for quadratic equation solution + // https://people.csail.mit.edu/bkph/articles/Quadratics.pdf + if (b > 0) + return (2 * c) / (-b - root2(d)); + else + return (2 * c) / (-b + root2(d)); +} + } // namespace ripple diff --git a/src/ripple/app/misc/impl/AMMUtils.cpp b/src/ripple/app/misc/impl/AMMUtils.cpp index cf4da2c5d85..4f6f0fbd3b5 100644 --- a/src/ripple/app/misc/impl/AMMUtils.cpp +++ b/src/ripple/app/misc/impl/AMMUtils.cpp @@ -348,4 +348,86 @@ initializeFeeAuctionVote( auctionSlot.makeFieldAbsent(sfDiscountedFee); // LCOV_EXCL_LINE } +Expected +isOnlyLiquidityProvider( + ReadView const& view, + Issue const& ammIssue, + AccountID const& lpAccount) +{ + // Liquidity Provider (LP) must have one LPToken trustline + std::uint8_t nLPTokenTrustLines = 0; + // There are at most two IOU trustlines. One or both could be to the LP + // if LP is the issuer, or a different account if LP is not an issuer. + // For instance, if AMM has two tokens USD and EUR and LP is not the issuer + // of the tokens then the trustlines are between AMM account and the issuer. + std::uint8_t nIOUTrustLines = 0; + // There is only one AMM object + bool hasAMM = false; + // AMM LP has at most three trustlines and only one AMM object must exist. + // If there are more than five objects then it's either an error or + // there are more than one LP. Ten pages should be sufficient to include + // five objects. + std::uint8_t limit = 10; + auto const root = keylet::ownerDir(ammIssue.account); + auto currentIndex = root; + + // Iterate over AMM owner directory objects. + while (limit-- >= 1) + { + auto const ownerDir = view.read(currentIndex); + if (!ownerDir) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + for (auto const& key : ownerDir->getFieldV256(sfIndexes)) + { + auto const sle = view.read(keylet::child(key)); + if (!sle) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + // Only one AMM object + if (sle->getFieldU16(sfLedgerEntryType) == ltAMM) + { + if (hasAMM) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + hasAMM = true; + continue; + } + if (sle->getFieldU16(sfLedgerEntryType) != ltRIPPLE_STATE) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + auto const lowLimit = sle->getFieldAmount(sfLowLimit); + auto const highLimit = sle->getFieldAmount(sfHighLimit); + auto const isLPTrustline = lowLimit.getIssuer() == lpAccount || + highLimit.getIssuer() == lpAccount; + auto const isLPTokenTrustline = + lowLimit.issue() == ammIssue || highLimit.issue() == ammIssue; + + // Liquidity Provider trustline + if (isLPTrustline) + { + // LPToken trustline + if (isLPTokenTrustline) + { + if (++nLPTokenTrustLines > 1) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + } + else if (++nIOUTrustLines > 2) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + } + // Another Liquidity Provider LPToken trustline + else if (isLPTokenTrustline) + return false; + else if (++nIOUTrustLines > 2) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + } + auto const uNodeNext = ownerDir->getFieldU64(sfIndexNext); + if (uNodeNext == 0) + { + if (nLPTokenTrustLines != 1 || nIOUTrustLines == 0 || + nIOUTrustLines > 2) + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE + return true; + } + currentIndex = keylet::page(root, uNodeNext); + } + return Unexpected(tecINTERNAL); // LCOV_EXCL_LINE +} + } // namespace ripple diff --git a/src/ripple/app/paths/impl/AMMLiquidity.cpp b/src/ripple/app/paths/impl/AMMLiquidity.cpp index bcc086e23da..9ec23d08a1a 100644 --- a/src/ripple/app/paths/impl/AMMLiquidity.cpp +++ b/src/ripple/app/paths/impl/AMMLiquidity.cpp @@ -205,8 +205,8 @@ AMMLiquidity::getOffer( return maxOffer(balances, view.rules()); } else if ( - auto const amounts = - changeSpotPriceQuality(balances, *clobQuality, tradingFee_)) + auto const amounts = changeSpotPriceQuality( + balances, *clobQuality, tradingFee_, view.rules(), j_)) { return AMMOffer( *this, *amounts, balances, Quality{*amounts}); @@ -239,7 +239,12 @@ AMMLiquidity::getOffer( return offer; } - JLOG(j_.error()) << "AMMLiquidity::getOffer, failed"; + JLOG(j_.error()) << "AMMLiquidity::getOffer, failed " + << ammContext_.multiPath() << " " + << ammContext_.curIters() << " " + << (clobQuality ? clobQuality->rate() : STAmount{}) + << " " << to_string(balances.in) << " " + << to_string(balances.out); } return std::nullopt; diff --git a/src/ripple/app/paths/impl/BookStep.cpp b/src/ripple/app/paths/impl/BookStep.cpp index 358dac4c796..4a43d653e0c 100644 --- a/src/ripple/app/paths/impl/BookStep.cpp +++ b/src/ripple/app/paths/impl/BookStep.cpp @@ -275,6 +275,7 @@ class BookPaymentStep : public BookStep> using BookStep>::BookStep; using BookStep>::qualityUpperBound; + using typename BookStep>::OfferType; // Never limit self cross quality on a payment. template