fix: AMM Quality Leak into Domain BookStep for Permissioned DEX (#6853)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Zhiyuan Wang
2026-06-24 08:15:45 -04:00
committed by GitHub
parent 6341e75200
commit 69d289a388
2 changed files with 84 additions and 0 deletions

View File

@@ -905,6 +905,11 @@ BookStep<TIn, TOut, TDerived>::getAMMOffer(
ReadView const& view,
std::optional<Quality> const& clobQuality) const
{
// AMM doesn't support domain books. When fixCleanup3_3_0 is enabled, exclude
// AMM liquidity so quality estimation matches actual crossing (tryAMM skips
// AMM for domain books).
if (book_.domain && view.rules().enabled(fixCleanup3_3_0))
return std::nullopt;
if (ammLiquidity_)
return ammLiquidity_->getOffer(view, clobQuality);
return std::nullopt;

View File

@@ -993,6 +993,83 @@ class PermissionedDEX_test : public beast::unit_test::Suite
BEAST_EXPECT(usd == USD(45));
}
void
testAmmQualityNotLeaked(FeatureBitset features)
{
bool const excludesAmmFromDomainQuality = features[fixCleanup3_3_0];
testcase << "AMM quality not leaked into domain BookStep"
<< (excludesAmmFromDomainQuality ? " (Cleanup3_3_0 enabled)"
: " (Cleanup3_3_0 disabled)");
Env env(*this, features);
auto const& [gw, domainOwner, alice, bob, carol, USD, domainID, credType] =
PermissionedDEX(env);
auto const eur = gw["EUR"];
env.trust(eur(1000), bob, domainOwner);
env.close();
env(pay(gw, bob, eur(100)));
env.close();
env(pay(gw, alice, USD(500)));
env.close();
// The AMM makes the direct XRP->USD book look much better than it
// really is for domain payments. The domain LOB direct path is 1:1,
// while the competing XRP->EUR->USD path is 2:1.
AMM const amm(env, alice, XRP(10), USD(500));
auto const directOfferSeq{env.seq(bob)};
env(offer(bob, XRP(10), USD(10)), Domain(domainID));
env.close();
auto const xrpEurOfferSeq{env.seq(bob)};
env(offer(bob, XRP(10), eur(20)), Domain(domainID));
env.close();
auto const eurUsdOfferSeq{env.seq(domainOwner)};
env(offer(domainOwner, eur(20), USD(20)), Domain(domainID));
env.close();
auto const carolBalBefore = env.balance(carol, USD);
// Both paths compete for the same XRP(10) sendmax. If AMM quality leaks
// into the direct domain book, the engine ranks direct XRP->USD first
// but crossing can only consume the 1:1 LOB offer. With the fix, the
// direct book is ranked by its domain LOB quality, so the 2:1
// XRP->EUR->USD path executes first.
env(pay(alice, carol, USD(100)),
Path(~USD),
Path(~eur, ~USD),
Sendmax(XRP(10)),
Txflags(tfPartialPayment | tfNoRippleDirect),
Domain(domainID));
env.close();
auto const delivered = env.balance(carol, USD) - carolBalBefore;
if (excludesAmmFromDomainQuality)
{
BEAST_EXPECT(delivered == USD(20));
BEAST_EXPECT(checkOffer(env, bob, directOfferSeq, XRP(10), USD(10), 0, true));
BEAST_EXPECT(!offerExists(env, bob, xrpEurOfferSeq));
BEAST_EXPECT(!offerExists(env, domainOwner, eurUsdOfferSeq));
}
else
{
BEAST_EXPECT(delivered == USD(10));
BEAST_EXPECT(!offerExists(env, bob, directOfferSeq));
BEAST_EXPECT(checkOffer(env, bob, xrpEurOfferSeq, XRP(10), eur(20), 0, true));
BEAST_EXPECT(checkOffer(env, domainOwner, eurUsdOfferSeq, eur(20), USD(20), 0, true));
}
auto [xrp, usd, lpt] = amm.balances(XRP, USD);
BEAST_EXPECT(xrp == XRP(10));
BEAST_EXPECT(usd == USD(500));
}
void
testHybridOfferCreate(FeatureBitset features)
{
@@ -1943,6 +2020,8 @@ public:
testOfferTokenIssuerInDomain(all);
testRemoveUnfundedOffer(all);
testAmmNotUsed(all);
testAmmQualityNotLeaked(all);
testAmmQualityNotLeaked(all - fixCleanup3_3_0);
testAutoBridge(all);
// Test hybrid offers