mirror of
https://github.com/XRPLF/rippled.git
synced 2026-06-03 16:56:48 +00:00
Defer MPT creation until offer consume
This commit is contained in:
@@ -740,18 +740,6 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
bool const isAssetInMPT = assetIn.holds<MPTIssue>();
|
||||
auto const& owner = offer.owner();
|
||||
|
||||
if (isAssetInMPT)
|
||||
{
|
||||
// Create MPToken for the offer's owner. No need to check
|
||||
// for the reserve since the offer is removed if it is consumed.
|
||||
// Therefore, the owner count remains the same.
|
||||
if (auto const err = checkCreateMPT(sb, assetIn.get<MPTIssue>(), owner, j_);
|
||||
!isTesSuccess(err))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
auto removeOffer = [&](std::string_view logMessage = {}) {
|
||||
auto const key = offer.key();
|
||||
if (!logMessage.empty())
|
||||
@@ -773,7 +761,11 @@ BookStep<TIn, TOut, TDerived>::forEachOffer(
|
||||
// Make sure offer owner has authorization to own Assets from issuer
|
||||
// and MPT assets can be traded/transferred.
|
||||
// An account can always own XRP or their own Assets.
|
||||
if (!isTesSuccess(requireAuth(applyView, assetIn, owner)) || !checkMPTDEX(sb, owner))
|
||||
// Missing MPTokens are allowed during offer discovery; they are
|
||||
// created later if the offer is actually consumed.
|
||||
auto const authType = isAssetInMPT ? AuthType::WeakAuth : AuthType::Legacy;
|
||||
if (!isTesSuccess(requireAuth(applyView, assetIn, owner, authType)) ||
|
||||
!checkMPTDEX(sb, owner))
|
||||
{
|
||||
// Offer owner not authorized to hold IOU/MPT from issuer.
|
||||
// Remove this offer even if no crossing occurs.
|
||||
@@ -921,6 +913,18 @@ BookStep<TIn, TOut, TDerived>::consumeOffer(
|
||||
// The offer owner gets the ofrAmt. The difference between ofrAmt and
|
||||
// stepAmt is a transfer fee that goes to book_.in.account
|
||||
{
|
||||
if constexpr (std::is_same_v<TIn, MPTAmount>)
|
||||
{
|
||||
// If the offer's TakerPays asset is an MPT, the offer owner must
|
||||
// hold an MPToken to receive it. Create one here, after the
|
||||
// consumption decision has been made, so the +1 to ownerCount is
|
||||
// paired atomically with the -1 that follows when the offer SLE is
|
||||
// deleted by BookTip::step().
|
||||
if (auto const err = checkCreateMPT(sb, book_.in.get<MPTIssue>(), offer.owner(), j_);
|
||||
!isTesSuccess(err))
|
||||
Throw<FlowException>(err);
|
||||
}
|
||||
|
||||
auto const dr = offer.send(
|
||||
sb, book_.in.getIssuer(), offer.owner(), toSTAmount(ofrAmt.in, book_.in), j_);
|
||||
if (!isTesSuccess(dr))
|
||||
|
||||
@@ -741,6 +741,161 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
return result;
|
||||
}
|
||||
|
||||
void
|
||||
testOfferOwnerMPTCreation(FeatureBitset features)
|
||||
{
|
||||
using namespace jtx;
|
||||
Account const alice("alice");
|
||||
Account const bob("bob");
|
||||
Account const carol("carol");
|
||||
Account const gw("gw");
|
||||
|
||||
{
|
||||
testcase("Reserve-edge offer owner cannot create another object");
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
|
||||
auto const xrpOffer = ownerIncrement - drops(1);
|
||||
auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
|
||||
|
||||
env.fund(XRP(10'000), alice, gw);
|
||||
env.fund(bobStart, bob);
|
||||
env.close();
|
||||
|
||||
MPTTester const usd({.env = env, .issuer = gw, .maxAmt = 10});
|
||||
|
||||
env(offer(bob, usd(1), xrpOffer));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
|
||||
|
||||
// This mirrors the full-crossing setup below. Bob has enough XRP
|
||||
// for the resting offer, but not enough to pay a fee and add
|
||||
// another owner-count object while the offer remains on ledger.
|
||||
env(check::create(bob, alice, drops(1)), Ter(tecINSUFFICIENT_RESERVE));
|
||||
env.close();
|
||||
|
||||
env.require(Owners(bob, 1));
|
||||
BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Reserve-edge offer owner creates MPToken during consume");
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
|
||||
auto const xrpOffer = ownerIncrement - drops(1);
|
||||
auto const bobStart = reserve(env, 2) - drops(1) + baseFee;
|
||||
|
||||
env.fund(XRP(10'000), alice, carol, gw);
|
||||
env.fund(bobStart, bob);
|
||||
env.close();
|
||||
|
||||
MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
|
||||
|
||||
env(pay(gw, alice, usd(1)));
|
||||
env(offer(bob, usd(1), xrpOffer));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(bob, reserve(env, 2) - drops(1)), Owners(bob, 1));
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
|
||||
auto const carolXRP = env.balance(carol);
|
||||
|
||||
// Bob has enough XRP for the resting offer but is close to
|
||||
// reserve. The payment should not create Bob's USD MPToken until
|
||||
// the offer is actually consumed, otherwise the temporary owner
|
||||
// count increase can make the offer look underfunded during path
|
||||
// execution.
|
||||
env(pay(alice, carol, xrpOffer),
|
||||
Path(~XRP),
|
||||
Sendmax(usd(1)),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(carol, carolXRP + xrpOffer));
|
||||
env.require(Balance(bob, usd(1)));
|
||||
env.require(Balance(bob, reserve(env, 1)), Owners(bob, 1));
|
||||
BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
|
||||
BEAST_EXPECT(offersOnAccount(env, bob).empty());
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Partial offer owner creates MPToken during consume");
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
auto const baseFee = env.current()->fees().base;
|
||||
auto const ownerIncrement = reserve(env, 1) - reserve(env, 0);
|
||||
auto const bobStart = reserve(env, 3) + baseFee;
|
||||
|
||||
env.fund(XRP(10'000), alice, carol, gw);
|
||||
env.fund(bobStart, bob);
|
||||
env.close();
|
||||
|
||||
MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
|
||||
|
||||
env(pay(gw, alice, usd(1)));
|
||||
env(offer(bob, usd(2), drops(2 * ownerIncrement)));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(bob, reserve(env, 3)), Owners(bob, 1));
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
|
||||
auto const carolXRP = env.balance(carol);
|
||||
|
||||
// Partial consumption leaves Bob's offer on ledger, so this setup
|
||||
// gives Bob enough reserve for both the remaining offer and the
|
||||
// newly created MPToken.
|
||||
env(pay(alice, carol, drops(ownerIncrement)),
|
||||
Path(~XRP),
|
||||
Sendmax(usd(1)),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(carol, carolXRP + drops(ownerIncrement)));
|
||||
env.require(Balance(bob, usd(1)));
|
||||
env.require(Balance(bob, reserve(env, 2)), Owners(bob, 2));
|
||||
BEAST_EXPECT(env.le(keylet::mptoken(usd.issuanceID(), bob.id())));
|
||||
BEAST_EXPECT(offersOnAccount(env, bob).size() == 1);
|
||||
BEAST_EXPECT(isOffer(env, bob, usd(1), drops(ownerIncrement)));
|
||||
}
|
||||
|
||||
{
|
||||
testcase("Issuer-owned offer does not create issuer MPToken");
|
||||
|
||||
Env env(*this, features);
|
||||
|
||||
env.fund(XRP(10'000), alice, carol, gw);
|
||||
env.close();
|
||||
|
||||
MPTTester const usd({.env = env, .issuer = gw, .holders = {alice}, .maxAmt = 10});
|
||||
|
||||
env(pay(gw, alice, usd(1)));
|
||||
env(offer(gw, usd(1), drops(1'000)));
|
||||
env.close();
|
||||
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
|
||||
auto const carolXRP = env.balance(carol);
|
||||
|
||||
// The issuer can own an offer that receives its own MPT without an
|
||||
// MPToken. Consuming that offer should keep the issuer side
|
||||
// tokenless.
|
||||
env(pay(alice, carol, drops(1'000)),
|
||||
Path(~XRP),
|
||||
Sendmax(usd(1)),
|
||||
Txflags(tfNoRippleDirect));
|
||||
env.close();
|
||||
|
||||
env.require(Balance(alice, usd(0)));
|
||||
env.require(Balance(carol, carolXRP + drops(1'000)));
|
||||
BEAST_EXPECT(!env.le(keylet::mptoken(usd.issuanceID(), gw.id())));
|
||||
BEAST_EXPECT(offersOnAccount(env, gw).empty());
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
testSelfPayment1(FeatureBitset features)
|
||||
{
|
||||
@@ -2122,6 +2277,7 @@ struct FlowMPT_test : public beast::unit_test::Suite
|
||||
testFalseDry(features);
|
||||
testDirectStep(features);
|
||||
testBookStep(features);
|
||||
testOfferOwnerMPTCreation(features);
|
||||
testTransferRate(features);
|
||||
testSelfPayment1(features);
|
||||
testSelfPayment2(features);
|
||||
|
||||
Reference in New Issue
Block a user