fix: Assorted MPT/DEX fixes (#7299)

Co-authored-by: Valentin Balaschenko <13349202+vlntb@users.noreply.github.com>
This commit is contained in:
Gregory Tsipenyuk
2026-08-11 18:15:51 +00:00
committed by GitHub
parent 6ca2fb84d4
commit 26cc683ec1
25 changed files with 2198 additions and 179 deletions

View File

@@ -226,7 +226,7 @@ getAMMOfferStartWithTakerGets(
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.
// This has the most impact when takerGets is integral.
auto const takerGets =
toAmount<TOut>(getAsset(pool.out), nTakerGetsProposed, Number::RoundingMode::Downward);
return TAmounts<TIn, TOut>{swapAssetOut(pool, takerGets, tfee), takerGets};
@@ -294,7 +294,7 @@ getAMMOfferStartWithTakerPays(
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.
// This has the most impact when takerPays is integral.
auto const takerPays =
toAmount<TIn>(getAsset(pool.in), nTakerPaysProposed, Number::RoundingMode::Downward);
return TAmounts<TIn, TOut>{takerPays, swapAssetIn(pool, takerPays, tfee)};
@@ -313,11 +313,11 @@ getAMMOfferStartWithTakerPays(
* 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.
* Pre-amendment code calculates takerPays first. If takerGets is the
* economically coarser integral side, 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 economically coarser integral 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
@@ -396,10 +396,18 @@ changeSpotPriceQuality(
return std::nullopt;
}
// Generate the offer starting with XRP side. Return seated offer amounts
// if the offer can be generated, otherwise nullopt.
auto amounts = [&]() {
if (isXRP(getAsset(pool.out)))
bool const inIntegral = getAsset(pool.in).integral();
bool const outIntegral = getAsset(pool.out).integral();
// Preserve historical behavior for fractional pairs and XRP/IOU-style
// one-integral-side pairs. For two integral assets, pick the side whose
// minimum unit is economically coarser at this quality.
//
// Quality::rate() is input units per output unit, so one output unit is
// coarser when it costs at least one input unit. Ties use takerGets,
// matching the historical XRP-output behavior.
if (outIntegral && (!inIntegral || Number(quality.rate()) >= 1))
return getAMMOfferStartWithTakerGets(pool, quality, tfee);
return getAMMOfferStartWithTakerPays(pool, quality, tfee);
}();

View File

@@ -261,6 +261,14 @@ checkCreateMPT(
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
SLE::ref sponsorSle,
std::uint32_t flags,
beast::Journal j);
TER
checkCreateMPT(
xrpl::ApplyView& view,
xrpl::MPTIssue const& mptIssue,
xrpl::AccountID const& holder,
beast::Journal j);
//------------------------------------------------------------------------------

View File

@@ -154,7 +154,7 @@ T
toAmount(Asset const& asset, Number const& n, Number::RoundingMode mode = Number::getround())
{
SaveNumberRoundMode const rm(Number::getround());
if (isXRP(asset))
if (asset.integral())
Number::setround(mode);
if constexpr (std::is_same_v<IOUAmount, T>)

View File

@@ -60,6 +60,15 @@ public:
std::optional<Number>
outFromAvgQ(Quality const& quality);
/**
* Return whether `out` produces at least the requested
* average quality.
* @param quality requested average quality (quality limit)
* @param out output amount to test
*/
[[nodiscard]] bool
satisfiesAvgQ(Quality const& quality, Number const& out) const;
/**
* Return true if the quality function is constant
*/

View File

@@ -1,6 +1,7 @@
#pragma once
#include <xrpl/basics/Log.h>
#include <xrpl/basics/Number.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/beast/utility/Journal.h>
#include <xrpl/beast/utility/Zero.h>
@@ -373,7 +374,7 @@ qualityUpperBound(ReadView const& v, Strand const& strand)
* increases quality of AMM steps, increasing the strand's composite
* quality as the result.
*/
template <typename TOutAmt>
template <StepAmount TOutAmt>
inline TOutAmt
limitOut(
ReadView const& v,
@@ -411,21 +412,29 @@ limitOut(
auto const out = qf->outFromAvgQ(limitQuality);
if (!out)
return remainingOut;
if constexpr (std::is_same_v<TOutAmt, XRPAmount>)
if constexpr (std::is_same_v<TOutAmt, XRPAmount> || std::is_same_v<TOutAmt, MPTAmount>)
{
return XRPAmount{*out};
auto const roundedOut = TOutAmt{*out};
// Integral outputs that round above the continuous target can
// realize worse average quality than the requested limit. Keep the
// default rounded value when it still satisfies the limit, since it
// is the largest matching offer; otherwise round down.
if (v.rules().enabled(featureMPTokensV2) && roundedOut > *out &&
!qf->satisfiesAvgQ(limitQuality, roundedOut))
{
NumberRoundModeGuard const g(Number::RoundingMode::Downward);
return TOutAmt{*out};
}
return roundedOut;
}
else if constexpr (std::is_same_v<TOutAmt, IOUAmount>)
{
return IOUAmount{*out};
}
else if constexpr (std::is_same_v<TOutAmt, MPTAmount>)
{
return MPTAmount{*out};
}
else
{
return STAmount{remainingOut.asset(), out->mantissa(), out->exponent()};
static constexpr bool kAlwaysFalse = !std::is_same_v<TOutAmt, TOutAmt>;
static_assert(kAlwaysFalse, "Unhandled StepAmount type");
}
}();
// A tiny difference could be due to the round off

View File

@@ -118,6 +118,7 @@ public:
Sandbox& view,
SLE const& ammSle,
AccountID const account,
std::optional<AccountID> const& clawbackIssuer,
AccountID const& ammAccount,
STAmount const& amountBalance,
STAmount const& amount2Balance,
@@ -138,6 +139,11 @@ public:
* @param view
* @param ammSle AMM ledger entry
* @param ammAccount AMM account
* @param clawbackIssuer when set (AMMClawback path), the issuer performing
* the clawback. A recreated MPToken is only auto-authorized when the
* asset's issuer matches this account, so a clawback cannot grant
* authorization on behalf of a different (paired-asset) issuer.
* @param account LP account
* @param amountBalance current LP asset1 balance
* @param amountWithdraw asset1 withdraw amount
* @param amount2Withdraw asset2 withdraw amount
@@ -153,6 +159,7 @@ public:
Sandbox& view,
SLE const& ammSle,
AccountID const& ammAccount,
std::optional<AccountID> const& clawbackIssuer,
AccountID const& account,
STAmount const& amountBalance,
STAmount const& amountWithdraw,