From ba63f8d99f90de1eb21dc099e512fbb0fc857c31 Mon Sep 17 00:00:00 2001 From: Ed Hennis Date: Tue, 6 May 2025 21:26:38 -0400 Subject: [PATCH] Implement LoanDraw, and made good progress on related tests --- .../xrpl/protocol/detail/transactions.macro | 2 +- src/test/app/Loan_test.cpp | 240 +++++++++++++----- src/test/jtx/TestHelpers.h | 7 + src/test/jtx/impl/TestHelpers.cpp | 16 ++ src/xrpld/app/tx/detail/LoanDelete.cpp | 3 + src/xrpld/app/tx/detail/LoanDraw.cpp | 194 ++++++++++++++ src/xrpld/app/tx/detail/LoanDraw.h | 53 ++++ src/xrpld/app/tx/detail/LoanManage.cpp | 3 + 8 files changed, 447 insertions(+), 71 deletions(-) create mode 100644 src/xrpld/app/tx/detail/LoanDraw.cpp create mode 100644 src/xrpld/app/tx/detail/LoanDraw.h diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index ee08ce6462..6de416ea21 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -767,7 +767,6 @@ TRANSACTION(ttLOAN_MANAGE, 80, LoanManage, noPriv, ({ {sfLoanID, soeREQUIRED}, })) -#if 0 /** The Borrower uses this transaction to draws funds from the Loan. */ #if TRANSACTION_INCLUDE # include @@ -777,6 +776,7 @@ TRANSACTION(ttLOAN_DRAW, 81, LoanDraw, noPriv, ({ {sfAmount, soeREQUIRED, soeMPTSupported}, })) +#if 0 /** The Borrower uses this transaction to make a Payment on the Loan. */ #if TRANSACTION_INCLUDE # include diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 73bce4ef02..c08df91a06 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -89,11 +89,11 @@ class Loan_test : public beast::unit_test::suite env(del(alice, loanKeylet.key), ter(temDISABLED)); // 3. LoanManage env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); -#if 0 // 4. LoanDraw - env(draw(alice, loanKeylet.key, Number(500)), ter(temDISABLED)); + env(draw(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); +#if 0 // 5. LoanPay - env(pay(alice, loanKeylet.key, Number(500)), ter(temDISABLED)); + env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); #endif }; failAll(all - featureMPTokensV1); @@ -112,6 +112,18 @@ class Loan_test : public beast::unit_test::suite } }; + struct LoanState + { + std::uint32_t previousPaymentDate = 0; + NetClock::time_point startDate = {}; + std::uint32_t nextPaymentDate = 0; + std::uint32_t paymentRemaining = 0; + Number assetsAvailable = 0; + Number principalOutstanding = 0; + std::uint32_t flags = 0; + std::uint32_t paymentInterval = 0; + }; + struct VerifyLoanStatus { public: @@ -184,6 +196,21 @@ class Loan_test : public beast::unit_test::suite } } + void + checkBroker( + LoanState const& state, + TenthBips32 interestRate, + std::uint32_t ownerCount) const + { + checkBroker( + state.assetsAvailable, + state.principalOutstanding, + interestRate, + state.paymentInterval, + state.paymentRemaining, + ownerCount); + } + void operator()( std::uint32_t previousPaymentDate, @@ -251,6 +278,18 @@ class Loan_test : public beast::unit_test::suite } } } + + void + operator()(LoanState const& state) const + { + operator()( + state.previousPaymentDate, + state.nextPaymentDate, + state.paymentRemaining, + state.assetsAvailable, + state.principalOutstanding, + state.flags); + }; }; void @@ -464,7 +503,8 @@ class Loan_test : public beast::unit_test::suite } return Number(0); }(); - auto const borrowerBalance = env.balance(borrower, broker.asset); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); // Try to delete the loan broker with an active loan env(loanBroker::del(lender, broker.brokerID), ter(tecHAS_OBLIGATIONS)); @@ -486,7 +526,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( env.balance(borrower, broker.asset).value() == - borrowerBalance.value() + assetsAvailable); + borrowerStartingBalance.value() + assetsAvailable); BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); @@ -528,9 +568,12 @@ class Loan_test : public beast::unit_test::suite std::string const iouCurrency{"IOU"}; PrettyAsset const iouAsset = issuer[iouCurrency]; env(trust(lender, iouAsset(1'000'000))); + env(trust(borrower, iouAsset(1'000'000))); env(trust(evan, iouAsset(1'000'000))); env(pay(issuer, evan, iouAsset(100'000))); env(pay(issuer, lender, iouAsset(100'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, iouAsset(1'000))); env.close(); MPTTester mptt{env, issuer, mptInitNoFund}; @@ -538,9 +581,12 @@ class Loan_test : public beast::unit_test::suite {.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock}); PrettyAsset const mptAsset = mptt.issuanceID(); mptt.authorize({.account = lender}); + mptt.authorize({.account = borrower}); mptt.authorize({.account = evan}); env(pay(issuer, lender, mptAsset(100'000))); env(pay(issuer, evan, mptAsset(100'000))); + // Fund the borrower with enough to cover interest and fees + env(pay(issuer, borrower, mptAsset(1'000))); env.close(); std::array const assets{xrpAsset, iouAsset, mptAsset}; @@ -606,6 +652,8 @@ class Loan_test : public beast::unit_test::suite using namespace loan; using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; Number const principalRequest = broker.asset(1000).value(); Number const maxCoveredLoanRequest = @@ -961,6 +1009,38 @@ class Loan_test : public beast::unit_test::suite // Finally! Create a loan std::string testData; + auto currentState = [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // Lookup the current loan state + LoanState state; + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + state.previousPaymentDate = loan->at(sfPreviousPaymentDate); + BEAST_EXPECT(state.previousPaymentDate == 0); + state.startDate = tp{d{loan->at(sfStartDate)}}; + state.nextPaymentDate = loan->at(sfNextPaymentDueDate); + BEAST_EXPECT( + tp{d{state.nextPaymentDate}} == state.startDate + 600s); + state.paymentRemaining = loan->at(sfPaymentRemaining); + BEAST_EXPECT(state.paymentRemaining == 12); + state.assetsAvailable = loan->at(sfAssetsAvailable); + BEAST_EXPECT( + state.assetsAvailable == broker.asset(999).value()); + state.principalOutstanding = + loan->at(sfPrincipalOutstanding); + BEAST_EXPECT( + state.principalOutstanding == + broker.asset(1000).value()); + state.paymentInterval = loan->at(sfPaymentInterval); + BEAST_EXPECT(state.paymentInterval == 600); + state.flags = loan->at(sfFlags); + } + + verifyLoanStatus(state); + + return state; + }; + auto defaultBeforeStartDate = [&](std::uint32_t baseFlag) { return [&, baseFlag]( Keylet const& loanKeylet, @@ -970,64 +1050,22 @@ class Loan_test : public beast::unit_test::suite // Default the loan // Initialize values with the current state - std::uint32_t previousPaymentDate = 0; - std::uint32_t nextPaymentDate = 0; - std::uint32_t paymentRemaining = 0; - Number assetsAvailable = 0; - Number principalOutstanding = 0; - std::uint32_t flags = 0; - std::uint32_t paymentInterval = 0; - - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - previousPaymentDate = loan->at(sfPreviousPaymentDate); - BEAST_EXPECT(previousPaymentDate == 0); - nextPaymentDate = loan->at(sfNextPaymentDueDate); - BEAST_EXPECT( - nextPaymentDate == loan->at(sfStartDate) + 600); - paymentRemaining = loan->at(sfPaymentRemaining); - BEAST_EXPECT(paymentRemaining == 12); - assetsAvailable = loan->at(sfAssetsAvailable); - BEAST_EXPECT( - assetsAvailable == broker.asset(999).value()); - principalOutstanding = loan->at(sfPrincipalOutstanding); - BEAST_EXPECT( - principalOutstanding == broker.asset(1000).value()); - paymentInterval = loan->at(sfPaymentInterval); - BEAST_EXPECT(paymentInterval == 600); - flags = loan->at(sfFlags); - BEAST_EXPECT(flags == baseFlag); - } - - verifyLoanStatus( - previousPaymentDate, - nextPaymentDate, - paymentRemaining, - assetsAvailable, - principalOutstanding, - flags); + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == baseFlag); // Impair the loan env(manage(lender, loanKeylet.key, tfLoanImpair)); - flags |= tfLoanImpair; - nextPaymentDate = env.now().time_since_epoch().count(); - verifyLoanStatus( - previousPaymentDate, - nextPaymentDate, - paymentRemaining, - assetsAvailable, - principalOutstanding, - flags); + state.flags |= tfLoanImpair; + state.nextPaymentDate = + env.now().time_since_epoch().count(); + verifyLoanStatus(state); // Once the loan is impaired, it can't be impaired again env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION)); - using d = NetClock::duration; - using tp = NetClock::time_point; - - auto const nextDueDate = tp{d{nextPaymentDate}}; + auto const nextDueDate = tp{d{state.nextPaymentDate}}; // Can't default the loan yet. The grace period hasn't // expired @@ -1038,20 +1076,22 @@ class Loan_test : public beast::unit_test::suite // defaulted env.close(nextDueDate + 60s); + // Impaired loans can't be drawn against + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + // Default the loan env(manage(lender, loanKeylet.key, tfLoanDefault)); - flags |= tfLoanDefault; - paymentRemaining = 0; - assetsAvailable = 0; - principalOutstanding = 0; - verifyLoanStatus( - previousPaymentDate, - nextPaymentDate, - paymentRemaining, - assetsAvailable, - principalOutstanding, - flags); + state.flags |= tfLoanDefault; + state.paymentRemaining = 0; + state.assetsAvailable = 0; + state.principalOutstanding = 0; + verifyLoanStatus(state); + + // Defaulted loans can't be drawn against, either + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); // Once a loan is defaulted, it can't be managed env(manage(lender, loanKeylet.key, tfLoanUnimpair), @@ -1086,9 +1126,8 @@ class Loan_test : public beast::unit_test::suite 0, defaultBeforeStartDate(0)); -#if 0 lifecycle( - "Loan overpayment allowed - Pay off", + "Loan overpayment allowed - Draw then default", env, lender, borrower, @@ -1100,13 +1139,74 @@ class Loan_test : public beast::unit_test::suite VerifyLoanStatus const& verifyLoanStatus) { // toEndOfLife // - // TODO: Draw and make some payments + // Initialize values with the current state + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == lsfLoanOverpayment); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); - // Make payments down to 0 + // Draw the balance + env(draw( + borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(draw(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(draw(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(draw(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + env(draw(borrower, loanKeylet.key, broker.asset(500)), + ter(tecTOO_SOON)); - // TODO: Try to impair a paid off loan + // Advance to the start date of the loan + env.close(state.startDate + 5s); + env(draw(borrower, loanKeylet.key, broker.asset(10000)), + ter(tecINSUFFICIENT_FUNDS)); + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(draw(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + verifyLoanStatus(state); + + // Draw about half the balance + auto const drawAmount = broker.asset(500); + env(draw(borrower, loanKeylet.key, drawAmount)); + + state.assetsAvailable -= drawAmount.number(); + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount); + + // move past the due date + env.close(tp{d{state.nextPaymentDate}} + 20s); + // Try to draw + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // default the loan + env(manage(lender, loanKeylet.key, tfLoanDefault)); + state.paymentRemaining = 0; + state.assetsAvailable = 0; + state.principalOutstanding = 0; + state.flags |= tfLoanDefault; + + verifyLoanStatus(state); + + // Same error, different check + env(draw(borrower, loanKeylet.key, broker.asset(100)), + ter(tecNO_PERMISSION)); + + // Default }); +#if 0 lifecycle( "Loan overpayment prohibited - Pay off", env, diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index 0f3d4b7455..e5b3f1f2d4 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -749,6 +749,13 @@ manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags); Json::Value del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0); +Json::Value +draw( + AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags = 0); + } // namespace loan } // namespace jtx diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index ae31d6d273..f2d9210212 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -513,6 +513,22 @@ del(AccountID const& account, uint256 const& loanID, std::uint32_t flags) return jv; } +Json::Value +draw( + AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanDraw; + jv[sfAccount] = to_string(account); + jv[sfLoanID] = to_string(loanID); + jv[sfAmount] = amount.getJson(); + jv[sfFlags] = flags; + return jv; +} + } // namespace loan } // namespace jtx } // namespace test diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index 06fa16d1fa..c2367392c8 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -53,6 +53,9 @@ LoanDelete::isEnabled(PreflightContext const& ctx) NotTEC LoanDelete::doPreflight(PreflightContext const& ctx) { + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + return tesSUCCESS; } diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp new file mode 100644 index 0000000000..3479b9898a --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -0,0 +1,194 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#include +// +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ripple { + +bool +LoanDraw::isEnabled(PreflightContext const& ctx) +{ + return LendingProtocolEnabled(ctx); +} + +NotTEC +LoanDraw::doPreflight(PreflightContext const& ctx) +{ + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + + if (ctx.tx[sfAmount] <= beast::zero) + return temBAD_AMOUNT; + + return tesSUCCESS; +} + +TER +LoanDraw::preclaim(PreclaimContext const& ctx) +{ + auto const& tx = ctx.tx; + + auto const account = tx[sfAccount]; + auto const loanID = tx[sfLoanID]; + auto const amount = tx[sfAmount]; + + auto const loanSle = ctx.view.read(keylet::loan(loanID)); + if (!loanSle) + { + JLOG(ctx.j.warn()) << "Loan does not exist."; + return tecNO_ENTRY; + } + + if (loanSle->at(sfBorrower) != account) + { + JLOG(ctx.j.warn()) << "Loan does not belong to the account."; + return tecNO_PERMISSION; + } + + if (loanSle->isFlag(lsfLoanImpaired) || loanSle->isFlag(lsfLoanDefault)) + { + JLOG(ctx.j.warn()) << "Loan is impaired or in default."; + return tecNO_PERMISSION; + } + + if (!hasExpired(ctx.view, loanSle->at(sfStartDate))) + { + JLOG(ctx.j.warn()) << "Loan has not started yet."; + return tecTOO_SOON; + } + + if (loanSle->at(sfAssetsAvailable) < amount) + { + JLOG(ctx.j.warn()) << "Loan does not have enough assets available."; + return tecINSUFFICIENT_FUNDS; + } + + auto const loanBrokerID = loanSle->at(sfLoanBrokerID); + auto const loanBrokerSle = ctx.view.read(keylet::loanbroker(loanBrokerID)); + if (!loanBrokerSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "LoanBroker does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const brokerPseudoAccount = loanBrokerSle->at(sfAccount); + auto const vaultID = loanBrokerSle->at(sfVaultID); + auto const vaultSle = ctx.view.read(keylet::vault(vaultID)); + if (!vaultSle) + { + // This should be impossible + // LCOV_EXCL_START + JLOG(ctx.j.fatal()) << "Vault does not exist."; + return tefBAD_LEDGER; + // LCOV_EXCL_STOP + } + auto const asset = vaultSle->at(sfAsset); + + if (amount.asset() != asset) + { + JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; + return tecWRONG_ASSET; + } + + if (isFrozen(ctx.view, brokerPseudoAccount, asset)) + { + JLOG(ctx.j.warn()) << "Loan Broker pseudo-account is frozen."; + return asset.holds() ? tecFROZEN : tecLOCKED; + } + if (asset.holds()) + { + auto const issue = asset.get(); + if (isDeepFrozen(ctx.view, account, issue.currency, issue.account)) + { + JLOG(ctx.j.warn()) << "Borrower account is frozen."; + return tecFROZEN; + } + } + + if (hasExpired(ctx.view, loanSle->at(sfNextPaymentDueDate))) + { + JLOG(ctx.j.warn()) << "Loan payment is overdue."; + return tecNO_PERMISSION; + } + + return tesSUCCESS; +} + +TER +LoanDraw::doApply() +{ + auto const& tx = ctx_.tx; + auto& view = ctx_.view(); + + auto const amount = tx[sfAmount]; + + auto const loanID = tx[sfLoanID]; + auto const loanSle = view.peek(keylet::loan(loanID)); + if (!loanSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + + auto const brokerID = loanSle->at(sfLoanBrokerID); + auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); + if (!brokerSle) + return tefBAD_LEDGER; // LCOV_EXCL_LINE + auto const brokerPseudoAccount = brokerSle->at(sfAccount); + + if (auto const ter = accountSend( + view, + brokerPseudoAccount, + account_, + amount, + j_, + WaiveTransferFee::Yes)) + return ter; + + loanSle->at(sfAssetsAvailable) -= amount; + view.update(loanSle); + + return tesSUCCESS; +} + +//------------------------------------------------------------------------------ + +} // namespace ripple diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h new file mode 100644 index 0000000000..bb8c5dcf22 --- /dev/null +++ b/src/xrpld/app/tx/detail/LoanDraw.h @@ -0,0 +1,53 @@ +//------------------------------------------------------------------------------ +/* + This file is part of rippled: https://github.com/ripple/rippled + Copyright (c) 2025 Ripple Labs Inc. + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ +//============================================================================== + +#ifndef RIPPLE_TX_LOANDRAW_H_INCLUDED +#define RIPPLE_TX_LOANDRAW_H_INCLUDED + +#include + +namespace ripple { + +class LoanDraw : public Transactor +{ +public: + static constexpr ConsequencesFactoryType ConsequencesFactory{Normal}; + + explicit LoanDraw(ApplyContext& ctx) : Transactor(ctx) + { + } + + static bool + isEnabled(PreflightContext const& ctx); + + static NotTEC + doPreflight(PreflightContext const& ctx); + + static TER + preclaim(PreclaimContext const& ctx); + + TER + doApply() override; +}; + +//------------------------------------------------------------------------------ + +} // namespace ripple + +#endif diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 70d78abbe8..f310aa6a19 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -60,6 +60,9 @@ LoanManage::getFlagsMask(PreflightContext const& ctx) NotTEC LoanManage::doPreflight(PreflightContext const& ctx) { + if (ctx.tx[sfLoanID] == beast::zero) + return temINVALID; + // Flags are mutually exclusive int numFlags = 0; for (auto const flag : {