mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 16:56:48 +00:00
Refine AMM offer generation for integral MPT pairs
This commit is contained in:
@@ -200,7 +200,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};
|
||||
@@ -215,7 +215,7 @@ getAMMOfferStartWithTakerGets(
|
||||
}
|
||||
|
||||
/** Generate AMM offer starting with takerPays when AMM pool
|
||||
* from the payment perspective is XRP(in)/IOU(out) or IOU(in)/IOU(out).
|
||||
* from the payment perspective has a fractional output asset.
|
||||
* Equations:
|
||||
* Spot Price Quality after the offer is consumed:
|
||||
* Qsp = (O - o) / (I + i) -- equation (1)
|
||||
@@ -267,7 +267,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)};
|
||||
@@ -285,11 +285,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
|
||||
@@ -368,10 +368,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);
|
||||
}();
|
||||
|
||||
@@ -5645,6 +5645,87 @@ private:
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
testAMMOfferGenerationPolicy(FeatureBitset features)
|
||||
{
|
||||
testcase("AMM payment offer generation picks economically coarser integral side");
|
||||
|
||||
using namespace jtx;
|
||||
|
||||
enum class GeneratedFirst { TakerPays, TakerGets };
|
||||
|
||||
auto const check = [&](std::uint64_t mptUnitsPerXRP, GeneratedFirst generatedFirst) {
|
||||
TAmounts<XRPAmount, MPTAmount> const pool{
|
||||
XRPAmount{1'000'000}, MPTAmount{1'000'000'125}};
|
||||
TAmounts<XRPAmount, MPTAmount> const clobOffer{
|
||||
kDROPS_PER_XRP, MPTAmount{static_cast<std::int64_t>(mptUnitsPerXRP)}};
|
||||
Quality const clobQuality{clobOffer};
|
||||
|
||||
auto const expectedAmounts = generatedFirst == GeneratedFirst::TakerGets
|
||||
? getAMMOfferStartWithTakerGets(pool, clobQuality, 0)
|
||||
: getAMMOfferStartWithTakerPays(pool, clobQuality, 0);
|
||||
auto const otherAmounts = generatedFirst == GeneratedFirst::TakerGets
|
||||
? getAMMOfferStartWithTakerPays(pool, clobQuality, 0)
|
||||
: getAMMOfferStartWithTakerGets(pool, clobQuality, 0);
|
||||
BEAST_EXPECT(expectedAmounts);
|
||||
BEAST_EXPECT(otherAmounts);
|
||||
if (!expectedAmounts || !otherAmounts)
|
||||
return;
|
||||
|
||||
// Make the tested branch observable: these cases are chosen so the
|
||||
// payment consumes different AMM amounts depending on which side
|
||||
// is generated first.
|
||||
BEAST_EXPECT(*expectedAmounts != *otherAmounts);
|
||||
|
||||
Env env(*this, features);
|
||||
auto const gw = Account("gw");
|
||||
auto const lp = Account("lp");
|
||||
auto const maker = Account("maker");
|
||||
auto const taker = Account("taker");
|
||||
auto const dst = Account("dst");
|
||||
|
||||
env.fund(XRP(10'000), gw, lp, maker, taker, dst);
|
||||
env.close();
|
||||
|
||||
MPTTester const token(
|
||||
{.env = env, .issuer = gw, .holders = {lp, maker, dst}, .flags = kMPT_DEX_FLAGS});
|
||||
env(pay(gw, lp, token(pool.out.value())));
|
||||
env(pay(gw, maker, token(10'000'000)));
|
||||
env.close();
|
||||
|
||||
AMM const amm(env, lp, drops(pool.in), token(pool.out.value()));
|
||||
auto const makerOfferSeq = env.seq(maker);
|
||||
env(offer(maker, XRP(1), token(mptUnitsPerXRP)), Txflags(tfPassive));
|
||||
env.close();
|
||||
|
||||
env(pay(taker, dst, token(expectedAmounts->out.value())),
|
||||
Sendmax(drops(expectedAmounts->in)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(amm.expectBalances(
|
||||
drops(pool.in + expectedAmounts->in),
|
||||
token((pool.out - expectedAmounts->out).value()),
|
||||
amm.tokens()));
|
||||
env.require(Balance(dst, token(expectedAmounts->out.value())));
|
||||
BEAST_EXPECT(env.le(keylet::offer(maker.id(), makerOfferSeq)));
|
||||
};
|
||||
|
||||
// CLOB price: 10'000'000 MPT per 1 XRP, so one raw MPT unit is worth
|
||||
// 0.1 drops. One drop is the economically coarser unit and the AMM
|
||||
// offer is generated from takerPays.
|
||||
check(10 * kDROPS_PER_XRP.drops(), GeneratedFirst::TakerPays);
|
||||
|
||||
// CLOB price: 1'000'000 MPT per 1 XRP, so one raw MPT unit is worth
|
||||
// one drop. Ties use takerGets to preserve the historical XRP-output
|
||||
// behavior.
|
||||
check(kDROPS_PER_XRP.drops(), GeneratedFirst::TakerGets);
|
||||
|
||||
// CLOB price: 100'000 MPT per 1 XRP, so one raw MPT unit is worth
|
||||
// 10 drops. MPT is the economically coarser unit and the AMM offer is
|
||||
// generated from takerGets.
|
||||
check(kDROPS_PER_XRP.drops() / 10, GeneratedFirst::TakerGets);
|
||||
}
|
||||
|
||||
void
|
||||
testTradingFee(FeatureBitset features)
|
||||
{
|
||||
@@ -7101,6 +7182,7 @@ private:
|
||||
testAMMTokens();
|
||||
testAmendment();
|
||||
testAMMAndCLOB(all);
|
||||
testAMMOfferGenerationPolicy(all);
|
||||
testTradingFee(all);
|
||||
testTradingFee(all - fixAMMv1_3);
|
||||
testAdjustedTokens(all);
|
||||
|
||||
Reference in New Issue
Block a user