From d3e22a125154068f18e665716e2e0861b0303ef8 Mon Sep 17 00:00:00 2001 From: Gregory Tsipenyuk Date: Tue, 21 Apr 2026 11:07:20 -0400 Subject: [PATCH] Add testAutoCreateReserve() unit-test to verify MPT/IOU consistent in not checking the reserves when creating MPToken or Trustline on partial offer crossing. --- src/test/app/OfferMPT_test.cpp | 96 ++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/test/app/OfferMPT_test.cpp b/src/test/app/OfferMPT_test.cpp index 8af583089a..e032af23ab 100644 --- a/src/test/app/OfferMPT_test.cpp +++ b/src/test/app/OfferMPT_test.cpp @@ -4702,6 +4702,101 @@ public: } } + void + testAutoCreateReserve(FeatureBitset features) + { + // When an offer on the book is partially crossed, the payment engine + // auto-creates a new ledger object (MPToken or IOU trustline) for the + // offer owner to hold the incoming asset. This happens inside + // BookStep::forEachOffer (MPT: checkCreateMPT) and BookStep::consumeOffer + // (IOU: directSendNoFeeIOU -> trustCreate) without a reserve sufficiency + // check. The offer owner can therefore end up with more objects than + // their XRP balance can reserve for, consistent with IOU behavior. + + testcase("Auto-Create Object Without Reserve Check During Partial Crossing"); + + using namespace jtx; + + auto const gw = Account{"gateway"}; + auto const alice = Account{"alice"}; + auto const carol = Account{"carol"}; + auto const bob = Account{"bob"}; + + auto test = [&](auto&& getToken, auto&& execTx) { + // MPT/IOU: carol's existing offer buys MPT/IOU by selling XRP. + // carol has no MPToken/Trustline for this issuance. When alice partially crosses + // carol's offer, an MPToken/Trustline is auto-created for carol without checking + // that she can afford the extra reserve slot. + Env env{*this, features}; + + auto const f = env.current()->fees().base; + auto const r = reserve(env, 0); + auto const inc = reserve(env, 1) - r; + + env.fund(XRP(10'000), gw, alice, bob); + + // getToken: + // - Create MPT with CanTransfer + CanTrade; authorize alice as holder. + // - Create IOU trustline + auto const Token = getToken(env); + + // carol: reserve(0) + 1 increment + fee covers placing one offer. + // After the offer tx she has exactly reserve(1) + XRP(30). + // XRP(30) < inc (50 XRP), so receiving a second object will put her + // below reserve(2). + if (BEAST_EXPECT(inc > XRP(30))) + env.fund(r + inc + f + XRP(30), carol); + + // carol's offer goes on the book (no counterpart yet). + // TakerPays=Token(30): carol will receive Token when crossed. + // TakerGets=XRP(30): carol will give XRP when crossed. + env(offer(carol, Token(30), XRP(30))); + env.require(owners(carol, 1)); + + // Execute offer create or cross-currency payment + // alice partially crosses carol's offer. + // alice sends Token(15) to carol and receives XRP(15). + // Token: + // - MPT: checkCreateMPT auto-creates an MPToken for carol (no reserve check). + // - IOU: directSendNoFeeIOU auto-creates an Trustline for carol (no reserve check). + execTx(env, Token); + + // Carol now owns 2 objects (remaining offer + new MPToken) even + // though her XRP balance is only reserve(1) + XRP(15), which is + // below reserve(2) = reserve(1) + inc. + auto const carolBalance = r + inc + XRP(15); + env.require(owners(carol, 2), balance(carol, Token(15)), balance(carol, carolBalance)); + BEAST_EXPECT(carolBalance < r + 2 * inc); // below reserve(2) + }; + std::function const getIOU = [&](Env& env) -> PrettyAsset { + env.trust(gw["USD"](1'000), alice); + env(pay(gw, alice, gw["USD"](100))); + return gw["USD"]; + }; + std::function const getMPT = [&](Env& env) -> PrettyAsset { + MPT const MPT1 = MPTTester({.env = env, .issuer = gw, .holders = {alice}, .pay = 100}); + return MPT1; + }; + for (auto&& getToken : {getIOU, getMPT}) + { + test(getToken, [&](Env& env, PrettyAsset const& Token) { + // alice partially crosses carol's offer. + // alice sends Token(15) to carol and receives XRP(15). + // Token is MPT: checkCreateMPT auto-creates an MPToken for carol (no reserve + // check). Token is IOU: directSendNoFeeIOU auto-creates a trustline for carol (no + // reserve check). + env(offer(alice, XRP(15), Token(15))); + }); + test(getToken, [&](Env& env, PrettyAsset const& Token) { + // Similar to above but with cross-currency payment. + env(pay(alice, bob, XRP(15)), + sendmax(Token(15)), + path(~XRP), + txflags(tfNoRippleDirect | tfPartialPayment)); + }); + } + } + void testAll(FeatureBitset features) { @@ -4758,6 +4853,7 @@ public: testRmSmallIncreasedQOffersMPT(features); testFillOrKill(features); testTickSize(features); + testAutoCreateReserve(features); } void