Remove LoanDraw transaction (#5792)

This commit is contained in:
Ed Hennis
2025-09-15 20:30:02 -04:00
committed by GitHub
parent dee972e9cd
commit 49eb1cc54e
13 changed files with 536 additions and 360 deletions

View File

@@ -558,7 +558,11 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfPreviousPaymentDate, soeREQUIRED},
{sfNextPaymentDueDate, soeREQUIRED},
{sfPaymentRemaining, soeREQUIRED},
//#if LOANDRAW
// TODO: Remove this when you remove the rest of the LOANDRAW blocks.
// Directives don't work with macro expansion.
{sfAssetsAvailable, soeDEFAULT},
//#endif
{sfPrincipalOutstanding, soeREQUIRED},
// Save the original request amount for rounding / scaling of
// other computations, particularly for IOUs

View File

@@ -1031,7 +1031,6 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet,
{sfCloseInterestRate, soeOPTIONAL},
{sfOverpaymentInterestRate, soeOPTIONAL},
{sfPrincipalRequested, soeREQUIRED},
{sfStartDate, soeREQUIRED},
{sfPaymentTotal, soeOPTIONAL},
{sfPaymentInterval, soeOPTIONAL},
{sfGracePeriod, soeOPTIONAL},
@@ -1059,6 +1058,7 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage,
{sfLoanID, soeREQUIRED},
}))
#if LOANDRAW
/** The Borrower uses this transaction to draws funds from the Loan. */
#if TRANSACTION_INCLUDE
# include <xrpld/app/tx/detail/LoanDraw.h>
@@ -1070,6 +1070,7 @@ TRANSACTION(ttLOAN_DRAW, 83, LoanDraw,
{sfLoanID, soeREQUIRED},
{sfAmount, soeREQUIRED, soeMPTSupported},
}))
#endif
/** The Borrower uses this transaction to make a Payment on the Loan. */
#if TRANSACTION_INCLUDE

View File

@@ -2631,10 +2631,7 @@ class Batch_test : public beast::unit_test::suite
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender,
brokerKeylet.key,
asset(1000).value(),
env.now() + 3600s),
set(lender, brokerKeylet.key, asset(1000).value()),
// Not allowed to include the counterparty signature
sig(sfCounterpartySignature, borrower),
sig(none),
@@ -2642,8 +2639,7 @@ class Batch_test : public beast::unit_test::suite
seq(none)),
lenderSeq + 1),
batch::inner(
draw(
lender,
pay(lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2));
@@ -2655,18 +2651,14 @@ class Batch_test : public beast::unit_test::suite
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender,
brokerKeylet.key,
asset(1000).value(),
env.now() + 3600s),
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must be set
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
draw(
lender,
pay(lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2));
@@ -2678,10 +2670,7 @@ class Batch_test : public beast::unit_test::suite
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender,
brokerKeylet.key,
asset(1000).value(),
env.now() + 3600s),
set(lender, brokerKeylet.key, asset(1000).value()),
// Counterparty must sign the outer transaction
counterparty(borrower.id()),
sig(none),
@@ -2689,8 +2678,7 @@ class Batch_test : public beast::unit_test::suite
seq(none)),
lenderSeq + 1),
batch::inner(
draw(
lender,
pay(lender,
loanKeylet.key,
STAmount{asset, asset(500).value()}),
lenderSeq + 2));
@@ -2706,17 +2694,14 @@ class Batch_test : public beast::unit_test::suite
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender,
brokerKeylet.key,
asset(1000).value(),
env.now() + 3600s),
set(lender, brokerKeylet.key, asset(1000).value()),
counterparty(borrower.id()),
sig(none),
fee(none),
seq(none)),
lenderSeq + 1),
batch::inner(
draw(
pay(
// However, this inner transaction will fail,
// because the lender is not allowed to draw the
// transaction
@@ -2741,10 +2726,7 @@ class Batch_test : public beast::unit_test::suite
batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing),
batch::inner(
env.json(
set(lender,
brokerKeylet.key,
asset(1000).value(),
env.now() + 3600s),
set(lender, brokerKeylet.key, asset(1000).value()),
counterparty(borrower.id()),
sig(none),
fee(none),

View File

@@ -3503,7 +3503,7 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000));
BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000));
// bob can finish escrow
env(escrow::finish(bob, alice, seq1),
@@ -3522,7 +3522,7 @@ struct EscrowToken_test : public beast::unit_test::suite
: MPT(20'000);
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix);
BEAST_EXPECT(env.balance(gw, MPT) == outstandingWithFix);
BEAST_EXPECT(env.balance(gw, MPT) == -outstandingWithFix);
}
// test locked rate: cancel
@@ -3567,7 +3567,7 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(alice, MPT) == preAlice);
BEAST_EXPECT(env.balance(bob, MPT) == preBob);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000));
BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000));
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
}
@@ -3608,7 +3608,7 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000));
BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000));
// bob can finish escrow
env(escrow::finish(gw, alice, seq1),
@@ -3620,7 +3620,7 @@ struct EscrowToken_test : public beast::unit_test::suite
BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta);
BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0);
BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0);
BEAST_EXPECT(env.balance(gw, MPT) == MPT(19'875));
BEAST_EXPECT(env.balance(gw, MPT) == MPT(-19'875));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -793,7 +793,6 @@ Json::Value
set(AccountID const& account,
uint256 const& loanBrokerID,
Number principalRequested,
NetClock::time_point const& startDate,
std::uint32_t flags = 0);
auto const counterparty = JTxFieldWrapper<accountIDField>(sfCounterparty);
@@ -835,12 +834,14 @@ 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);
#if loandraw
Json::Value
draw(
AccountID const& account,
uint256 const& loanID,
STAmount const& amount,
std::uint32_t flags = 0);
#endif
Json::Value
pay(AccountID const& account,

View File

@@ -450,7 +450,6 @@ Json::Value
set(AccountID const& account,
uint256 const& loanBrokerID,
Number principalRequested,
NetClock::time_point const& startDate,
std::uint32_t flags)
{
Json::Value jv;
@@ -459,7 +458,6 @@ set(AccountID const& account,
jv[sfLoanBrokerID] = to_string(loanBrokerID);
jv[sfPrincipalRequested] = to_string(principalRequested);
jv[sfFlags] = flags;
jv[sfStartDate] = startDate.time_since_epoch().count();
return jv;
}
@@ -485,6 +483,7 @@ del(AccountID const& account, uint256 const& loanID, std::uint32_t flags)
return jv;
}
#if LOANDRAW
Json::Value
draw(
AccountID const& account,
@@ -500,6 +499,7 @@ draw(
jv[sfFlags] = flags;
return jv;
}
#endif
Json::Value
pay(AccountID const& account,

View File

@@ -121,9 +121,10 @@ LoanDelete::doApply()
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultAsset = vaultSle->at(sfAsset);
#if LOANDRAW
// transfer any remaining funds to the borrower
auto const vaultAsset = vaultSle->at(sfAsset);
auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable);
if (assetsAvailableProxy != 0)
{
@@ -136,6 +137,7 @@ LoanDelete::doApply()
WaiveTransferFee::Yes))
return ter;
}
#endif
// Remove LoanID from Directory of the LoanBroker pseudo-account.
if (!view.dirRemove(

View File

@@ -1,3 +1,4 @@
#if LOANDRAW
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
@@ -121,7 +122,7 @@ LoanDraw::preclaim(PreclaimContext const& ctx)
if (amount.asset() != asset)
{
JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset.";
JLOG(ctx.j.warn()) << "Draw amount does not match the Vault asset.";
return tecWRONG_ASSET;
}
@@ -189,3 +190,4 @@ LoanDraw::doApply()
//------------------------------------------------------------------------------
} // namespace ripple
#endif

View File

@@ -1,3 +1,4 @@
#if LOANDRAW
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
@@ -51,3 +52,4 @@ public:
} // namespace ripple
#endif
#endif

View File

@@ -168,10 +168,15 @@ defaultLoan(
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
auto const totalDefaultAmount = principalOutstanding + interestOutstanding;
#if LOANDRAW
// The default Amount equals the outstanding principal and interest,
// excluding any funds unclaimed by the Borrower.
auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable);
auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy;
#else
// TODO: get rid of this and just use totalDefaultAmount
auto const defaultAmount = totalDefaultAmount;
#endif
// Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{
@@ -185,7 +190,11 @@ defaultLoan(
coverRateLiquidation),
defaultAmount),
originalPrincipalRequested);
#if LOANDRAW
auto const returnToVault = defaultCovered + loanAssetsAvailableProxy;
#else
auto const returnToVault = defaultCovered;
#endif
auto const vaultDefaultAmount = defaultAmount - defaultCovered;
// Update the Vault object:
@@ -250,7 +259,9 @@ defaultLoan(
// Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
loanSle->at(sfPaymentRemaining) = 0;
#if LOANDRAW
loanAssetsAvailableProxy = 0;
#endif
loanSle->at(sfPrincipalOutstanding) = 0;
view.update(loanSle);

View File

@@ -82,7 +82,6 @@ LoanPay::preclaim(PreclaimContext const& ctx)
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
auto const paymentRemaining = loanSle->at(sfPaymentRemaining);
TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)};
auto const startDate = loanSle->at(sfStartDate);
if (loanSle->at(sfBorrower) != account)
{
@@ -90,12 +89,6 @@ LoanPay::preclaim(PreclaimContext const& ctx)
return tecNO_PERMISSION;
}
if (!hasExpired(ctx.view, startDate))
{
JLOG(ctx.j.warn()) << "Loan has not started yet.";
return tecTOO_SOON;
}
if (paymentRemaining == 0 || principalOutstanding == 0)
{
JLOG(ctx.j.warn()) << "Loan is already paid off.";

View File

@@ -187,12 +187,6 @@ LoanSet::preclaim(PreclaimContext const& ctx)
{
auto const& tx = ctx.tx;
if (auto const startDate(tx[sfStartDate]); hasExpired(ctx.view, startDate))
{
JLOG(ctx.j.warn()) << "Start date is in the past.";
return tecEXPIRED;
}
auto const account = tx[sfAccount];
auto const brokerID = tx[sfLoanBrokerID];
@@ -223,7 +217,6 @@ LoanSet::preclaim(PreclaimContext const& ctx)
return terNO_ACCOUNT;
}
auto const brokerPseudo = brokerSle->at(sfAccount);
auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID)));
if (!vault)
// Should be impossible
@@ -249,16 +242,6 @@ LoanSet::preclaim(PreclaimContext const& ctx)
JLOG(ctx.j.warn()) << "Borrower account is frozen.";
return ret;
}
// TODO: Remove when LoanDraw is combined with LoanSet
// brokerPseudo is eventually going to send funds to the borrower, so it
// can't be frozen now. It is also going to receive funds, so it can't be
// deep frozen, but being frozen is a prerequisite for being deep frozen, so
// checking the one is sufficient.
if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset))
{
JLOG(ctx.j.warn()) << "Broker pseudo-account account is frozen.";
return ret;
}
// brokerOwner is going to receive funds if there's an origination fee, so
// it can't be deep frozen
if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset))
@@ -360,16 +343,26 @@ LoanSet::doApply()
if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
// Create a holding for the borrower if one does not already exist.
// Account for the origination fee using two payments
//
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
// from vault pseudo-account to LoanBroker pseudo-account.
// from vault pseudo-account to the borrower.
// Create a holding for the borrower if one does not already exist.
if (auto const ter = addEmptyHolding(
view,
borrower,
borrowerSle->at(sfBalance).value().xrp(),
vaultAsset,
j_);
ter && ter != tecDUPLICATE)
// ignore tecDUPLICATE. That means the holding already exists, and
// is fine here
return ter;
if (auto const ter = accountSend(
view,
vaultPseudo,
brokerPseudo,
borrower,
STAmount{vaultAsset, loanAssetsAvailable},
j_,
WaiveTransferFee::Yes))
@@ -414,7 +407,7 @@ LoanSet::doApply()
paymentInterval,
paymentTotal,
managementFeeRate);
auto const startDate = tx[sfStartDate];
auto const startDate = view.info().closeTime.time_since_epoch().count();
auto loanSequence = brokerSle->at(sfLoanSequence);
// Create the loan
@@ -453,7 +446,6 @@ LoanSet::doApply()
loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = paymentTotal;
loan->at(sfAssetsAvailable) = loanAssetsAvailable;
view.insert(loan);
// Update the balances in the vault