Don't create empty holding for MPT or trust line issuer (#5877)

- Adds a check to the MPToken creation invariant to ensure none are created for the issuer.
- `addEmptyHolding()` will return success without doing anything for these scenarios. There is nothing to do, as with XRP.

---------

Co-authored-by: Ed Hennis <ed@ripple.com>
This commit is contained in:
Gregory Tsipenyuk
2025-10-24 12:32:26 -04:00
committed by GitHub
parent 78ef800e30
commit 88a770c71b
4 changed files with 74 additions and 4 deletions

View File

@@ -1406,8 +1406,8 @@ addEmptyHolding(
Issue const& issue,
beast::Journal journal)
{
// Every account can hold XRP.
if (issue.native())
// Every account can hold XRP. An issuer can issue directly.
if (issue.native() || accountID == issue.getIssuer())
return tesSUCCESS;
auto const& issuerId = issue.getIssuer();
@@ -1468,6 +1468,8 @@ addEmptyHolding(
return tefINTERNAL; // LCOV_EXCL_LINE
if (view.peek(keylet::mptoken(mptID, accountID)))
return tecDUPLICATE;
if (accountID == mptIssue.getIssuer())
return tesSUCCESS;
return authorizeMPToken(view, priorBalance, mptID, accountID, journal);
}

View File

@@ -2827,6 +2827,46 @@ class Loan_test : public beast::unit_test::suite
pass();
}
void
testIssuerLoan()
{
testcase << "Issuer Loan";
using namespace jtx;
using namespace loan;
Account const issuer("issuer");
Account const borrower = issuer;
Account const lender("lender");
Env env(*this);
env.fund(XRP(1'000), issuer, lender);
std::int64_t constexpr issuerBalance = 10'000'000;
MPTTester asset(
{.env = env,
.issuer = issuer,
.holders = {lender},
.pay = issuerBalance});
auto const broker = createVaultAndBroker(env, asset, lender, 200);
auto const loanSetFee = fee(env.current()->fees().base * 2);
// Create Loan
env(set(borrower, broker.brokerID, 200),
sig(sfCounterpartySignature, lender),
loanSetFee);
env.close();
// Issuer should not create MPToken
BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer)));
// Issuer "borrowed" 200, OutstandingAmount decreased by 200
BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance + 200));
// Pay Loan
auto const loanKeylet = keylet::loan(broker.brokerID, 1);
env(pay(borrower, loanKeylet.key, asset(200)));
env.close();
// Issuer "re-payed" 200, OutstandingAmount increased by 200
BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance));
}
void
testInvalidLoanDelete()
{
@@ -3049,6 +3089,7 @@ public:
void
run() override
{
testIssuerLoan();
testDisabled();
testSelfLoan();
testLifecycle();

View File

@@ -1479,7 +1479,12 @@ ValidMPTIssuance::visitEntry(
if (isDelete)
mptokensDeleted_++;
else if (!before)
{
mptokensCreated_++;
MPTIssue const mptIssue{after->at(sfMPTokenIssuanceID)};
if (mptIssue.getIssuer() == after->at(sfAccount))
mptCreatedByIssuer_ = true;
}
}
}
@@ -1493,6 +1498,25 @@ ValidMPTIssuance::finalize(
{
if (result == tesSUCCESS)
{
auto const& rules = view.rules();
[[maybe_unused]]
bool enforceCreatedByIssuer = rules.enabled(featureSingleAssetVault) ||
rules.enabled(featureLendingProtocol);
if (mptCreatedByIssuer_)
{
JLOG(j.fatal())
<< "Invariant failed: MPToken created for the MPT issuer";
// The comment above starting with "assert(enforce)" explains this
// assert.
XRPL_ASSERT_PARTS(
enforceCreatedByIssuer,
"ripple::ValidMPTIssuance::finalize",
"no issuer MPToken");
if (enforceCreatedByIssuer)
return false;
}
auto const txnType = tx.getTxnType();
if (hasPrivilege(tx, createMPTIssuance))
{
if (mptIssuancesCreated_ == 0)
@@ -1540,7 +1564,7 @@ ValidMPTIssuance::finalize(
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause
// non-amendment-gated side effects.
bool const enforceEscrowFinish = (tx.getTxnType() == ttESCROW_FINISH) &&
bool const enforceEscrowFinish = (txnType == ttESCROW_FINISH) &&
(view.rules().enabled(featureSingleAssetVault) ||
lendingProtocolEnabled);
if (hasPrivilege(tx, mustAuthorizeMPT | mayAuthorizeMPT) ||
@@ -1591,7 +1615,7 @@ ValidMPTIssuance::finalize(
return true;
}
if (tx.getTxnType() == ttESCROW_FINISH)
if (txnType == ttESCROW_FINISH)
{
// ttESCROW_FINISH may authorize an MPT, but it can't have the
// mayAuthorizeMPT privilege, because that may cause

View File

@@ -576,6 +576,9 @@ class ValidMPTIssuance
std::uint32_t mptokensCreated_ = 0;
std::uint32_t mptokensDeleted_ = 0;
// non-MPT transactions may attempt to create
// MPToken by an issuer
bool mptCreatedByIssuer_ = false;
public:
void