Update to match latest spec: compute interest, LoanBroker reserves

This commit is contained in:
Ed Hennis
2025-05-06 18:19:55 -04:00
parent f2178a38f3
commit bcece6d680
13 changed files with 397 additions and 210 deletions

View File

@@ -72,7 +72,7 @@ class LoanBroker_test : public beast::unit_test::suite
using namespace loanBroker; using namespace loanBroker;
// Can't create a loan broker regardless of whether the vault exists // Can't create a loan broker regardless of whether the vault exists
env(set(alice, keylet.key), fee(increment), ter(temDISABLED)); env(set(alice, keylet.key), ter(temDISABLED));
auto const brokerKeylet = auto const brokerKeylet =
keylet::loanbroker(alice.id(), env.seq(alice)); keylet::loanbroker(alice.id(), env.seq(alice));
// Other LoanBroker transactions are disabled, too. // Other LoanBroker transactions are disabled, too.
@@ -129,7 +129,7 @@ class LoanBroker_test : public beast::unit_test::suite
{ {
// Start with default values // Start with default values
auto jtx = env.jt(set(alice, vault.vaultID), fee(increment)); auto jtx = env.jt(set(alice, vault.vaultID));
// Modify as desired // Modify as desired
if (modifyJTx) if (modifyJTx)
jtx = modifyJTx(jtx); jtx = modifyJTx(jtx);
@@ -390,6 +390,8 @@ class LoanBroker_test : public beast::unit_test::suite
env.close(); env.close();
} }
auto const aliceOriginalCount = env.ownerCount(alice);
// Create and update Loan Brokers // Create and update Loan Brokers
for (auto const& vault : vaults) for (auto const& vault : vaults)
{ {
@@ -397,73 +399,56 @@ class LoanBroker_test : public beast::unit_test::suite
auto badKeylet = keylet::vault(alice.id(), env.seq(alice)); auto badKeylet = keylet::vault(alice.id(), env.seq(alice));
// Try some failure cases // Try some failure cases
// insufficient fee
env(set(evan, vault.vaultID), ter(telINSUF_FEE_P));
// not the vault owner // not the vault owner
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID), ter(tecNO_PERMISSION));
fee(increment),
ter(tecNO_PERMISSION));
// not a vault // not a vault
env(set(alice, badKeylet.key), fee(increment), ter(tecNO_ENTRY)); env(set(alice, badKeylet.key), ter(tecNO_ENTRY));
// flags are checked first // flags are checked first
env(set(evan, vault.vaultID, ~tfUniversal), env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG));
fee(increment),
ter(temINVALID_FLAG));
// field length validation // field length validation
// sfData: good length, bad account // sfData: good length, bad account
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
fee(increment),
data(std::string(maxDataPayloadLength, 'X')), data(std::string(maxDataPayloadLength, 'X')),
ter(tecNO_PERMISSION)); ter(tecNO_PERMISSION));
// sfData: too long // sfData: too long
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
fee(increment),
data(std::string(maxDataPayloadLength + 1, 'Y')), data(std::string(maxDataPayloadLength + 1, 'Y')),
ter(temINVALID)); ter(temINVALID));
// sfManagementFeeRate: good value, bad account // sfManagementFeeRate: good value, bad account
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate), managementFeeRate(maxManagementFeeRate),
fee(increment),
ter(tecNO_PERMISSION)); ter(tecNO_PERMISSION));
// sfManagementFeeRate: too big // sfManagementFeeRate: too big
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate + TenthBips16(10)), managementFeeRate(maxManagementFeeRate + TenthBips16(10)),
fee(increment),
ter(temINVALID)); ter(temINVALID));
// sfCoverRateMinimum: good value, bad account // sfCoverRateMinimum: good value, bad account
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate), coverRateMinimum(maxCoverRate),
fee(increment),
ter(tecNO_PERMISSION)); ter(tecNO_PERMISSION));
// sfCoverRateMinimum: too big // sfCoverRateMinimum: too big
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate + 1), coverRateMinimum(maxCoverRate + 1),
fee(increment),
ter(temINVALID)); ter(temINVALID));
// sfCoverRateLiquidation: good value, bad account // sfCoverRateLiquidation: good value, bad account
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate), coverRateLiquidation(maxCoverRate),
fee(increment),
ter(tecNO_PERMISSION)); ter(tecNO_PERMISSION));
// sfCoverRateLiquidation: too big // sfCoverRateLiquidation: too big
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate + 1), coverRateLiquidation(maxCoverRate + 1),
fee(increment),
ter(temINVALID)); ter(temINVALID));
// sfDebtMaximum: good value, bad account // sfDebtMaximum: good value, bad account
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
debtMaximum(Number(0)), debtMaximum(Number(0)),
fee(increment),
ter(tecNO_PERMISSION)); ter(tecNO_PERMISSION));
// sfDebtMaximum: overflow // sfDebtMaximum: overflow
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
debtMaximum(Number(1, 100)), debtMaximum(Number(1, 100)),
fee(increment),
ter(temINVALID)); ter(temINVALID));
// sfDebtMaximum: negative // sfDebtMaximum: negative
env(set(evan, vault.vaultID), env(set(evan, vault.vaultID),
debtMaximum(Number(-1)), debtMaximum(Number(-1)),
fee(increment),
ter(temINVALID)); ter(temINVALID));
std::string testData; std::string testData;
@@ -486,6 +471,9 @@ class LoanBroker_test : public beast::unit_test::suite
BEAST_EXPECT(broker->at(sfDebtMaximum) == 0); BEAST_EXPECT(broker->at(sfDebtMaximum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0); BEAST_EXPECT(broker->at(sfCoverRateMinimum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0); BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);
BEAST_EXPECT(
env.ownerCount(alice) == aliceOriginalCount + 2);
}, },
[&](SLE::const_ref broker) { [&](SLE::const_ref broker) {
// Modifications // Modifications
@@ -587,6 +575,8 @@ class LoanBroker_test : public beast::unit_test::suite
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum)); BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
}); });
} }
BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount);
} }
public: public:

View File

@@ -23,6 +23,7 @@
#include <test/jtx/mpt.h> #include <test/jtx/mpt.h>
#include <test/jtx/vault.h> #include <test/jtx/vault.h>
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/LoanSet.h> #include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
@@ -136,6 +137,8 @@ class Loan_test : public beast::unit_test::suite
Number const& assetsAvailable, Number const& assetsAvailable,
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 interestRate, TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
std::uint32_t ownerCount) const std::uint32_t ownerCount) const
{ {
using namespace jtx; using namespace jtx;
@@ -144,10 +147,12 @@ class Loan_test : public beast::unit_test::suite
{ {
TenthBips16 const managementFeeRate{ TenthBips16 const managementFeeRate{
brokerSle->at(sfManagementFeeRate)}; brokerSle->at(sfManagementFeeRate)};
auto const loanInterest = LoanInterestOutstanding( auto const loanInterest = LoanInterestOutstandingToVault(
broker.asset, broker.asset,
principalOutstanding, principalOutstanding,
interestRate, interestRate,
paymentInterval,
paymentsRemaining,
managementFeeRate); managementFeeRate);
auto const expectedDebt = principalOutstanding + loanInterest; auto const expectedDebt = principalOutstanding + loanInterest;
env.test.BEAST_EXPECT( env.test.BEAST_EXPECT(
@@ -204,8 +209,14 @@ class Loan_test : public beast::unit_test::suite
env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
auto const paymentInterval = loan->at(sfPaymentInterval);
checkBroker( checkBroker(
assetsAvailable, principalOutstanding, interestRate, 1); assetsAvailable,
principalOutstanding,
interestRate,
paymentInterval,
paymentRemaining,
1);
if (auto brokerSle = if (auto brokerSle =
env.le(keylet::loanbroker(broker.brokerID)); env.le(keylet::loanbroker(broker.brokerID));
@@ -215,17 +226,20 @@ class Loan_test : public beast::unit_test::suite
env.le(keylet::vault(brokerSle->at(sfVaultID))); env.le(keylet::vault(brokerSle->at(sfVaultID)));
env.test.BEAST_EXPECT(vaultSle)) env.test.BEAST_EXPECT(vaultSle))
{ {
if (flags & lsfLoanImpaired) if ((flags & lsfLoanImpaired) &&
!(flags & lsfLoanDefault))
{ {
TenthBips32 const managementFeeRate{ TenthBips32 const managementFeeRate{
brokerSle->at(sfManagementFeeRate)}; brokerSle->at(sfManagementFeeRate)};
env.test.BEAST_EXPECT( env.test.BEAST_EXPECT(
vaultSle->at(sfLossUnrealized) == vaultSle->at(sfLossUnrealized) ==
principalOutstanding + principalOutstanding +
LoanInterestOutstanding( LoanInterestOutstandingToVault(
broker.asset, broker.asset,
principalOutstanding, principalOutstanding,
interestRate, interestRate,
paymentInterval,
paymentRemaining,
managementFeeRate)); managementFeeRate));
} }
else else
@@ -276,7 +290,7 @@ class Loan_test : public beast::unit_test::suite
env, broker, pseudoAcct, keylet); env, broker, pseudoAcct, keylet);
// No loans yet // No loans yet
verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 0); verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0);
if (!BEAST_EXPECT(loanSequence != 0)) if (!BEAST_EXPECT(loanSequence != 0))
return; return;
@@ -294,6 +308,8 @@ class Loan_test : public beast::unit_test::suite
using namespace loan; using namespace loan;
using namespace std::chrono_literals; using namespace std::chrono_literals;
auto const borrowerOwnerCount = env.ownerCount(borrower);
auto const loanSetFee = fee(env.current()->fees().base * 2); auto const loanSetFee = fee(env.current()->fees().base * 2);
Number const principalRequest = broker.asset(1000).value(); Number const principalRequest = broker.asset(1000).value();
auto const startDate = env.now() + 3600s; auto const startDate = env.now() + 3600s;
@@ -409,18 +425,17 @@ class Loan_test : public beast::unit_test::suite
env(manage(lender, broker.brokerID, tfLoanImpair), ter(tecNO_ENTRY)); env(manage(lender, broker.brokerID, tfLoanImpair), ter(tecNO_ENTRY));
// Loan is unimpaired, can't unimpair it again // Loan is unimpaired, can't unimpair it again
env(manage(lender, keylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION)); env(manage(lender, keylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION));
// Loan is unimpaired, can't jump straight to default // Loan is unimpaired, it can go into default, but only after it's past
env(manage(lender, keylet.key, tfLoanDefault), ter(tecNO_PERMISSION)); // due
env(manage(lender, keylet.key, tfLoanDefault), ter(tecTOO_SOON));
// Impair the loan // Impair the loan
env(manage(lender, keylet.key, tfLoanImpair)); env(manage(lender, keylet.key, tfLoanImpair));
// Unimpair the loan // Unimpair the loan
env(manage(lender, keylet.key, tfLoanUnimpair)); env(manage(lender, keylet.key, tfLoanUnimpair));
auto const nextDueDate = hasExpired(*env.current(), interval) auto const nextDueDate =
? env.current()->parentCloseTime().time_since_epoch().count() + startDate.time_since_epoch().count() + interval;
interval
: interval;
env.close(); env.close();
@@ -432,8 +447,6 @@ class Loan_test : public beast::unit_test::suite
principalRequest, principalRequest,
loanFlags | 0); loanFlags | 0);
// TODO: Draw and make some payments
// Can't delete the loan yet. It has payments remaining. // Can't delete the loan yet. It has payments remaining.
env(del(lender, keylet.key), ter(tecHAS_OBLIGATIONS)); env(del(lender, keylet.key), ter(tecHAS_OBLIGATIONS));
@@ -469,11 +482,12 @@ class Loan_test : public beast::unit_test::suite
env.close(); env.close();
// No loans left // No loans left
verifyLoanStatus.checkBroker(0, 0, interest, 0); verifyLoanStatus.checkBroker(0, 0, interest, 1, 0, 0);
BEAST_EXPECT( BEAST_EXPECT(
env.balance(borrower, broker.asset).value() == env.balance(borrower, broker.asset).value() ==
borrowerBalance.value() + assetsAvailable); borrowerBalance.value() + assetsAvailable);
BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
BEAST_EXPECT(brokerSle)) BEAST_EXPECT(brokerSle))
@@ -962,13 +976,15 @@ class Loan_test : public beast::unit_test::suite
Number assetsAvailable = 0; Number assetsAvailable = 0;
Number principalOutstanding = 0; Number principalOutstanding = 0;
std::uint32_t flags = 0; std::uint32_t flags = 0;
std::uint32_t paymentInterval = 0;
if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
{ {
previousPaymentDate = loan->at(sfPreviousPaymentDate); previousPaymentDate = loan->at(sfPreviousPaymentDate);
BEAST_EXPECT(previousPaymentDate == 0); BEAST_EXPECT(previousPaymentDate == 0);
nextPaymentDate = loan->at(sfNextPaymentDueDate); nextPaymentDate = loan->at(sfNextPaymentDueDate);
BEAST_EXPECT(nextPaymentDate >= 600); BEAST_EXPECT(
BEAST_EXPECT(nextPaymentDate < loan->at(sfStartDate)); nextPaymentDate == loan->at(sfStartDate) + 600);
paymentRemaining = loan->at(sfPaymentRemaining); paymentRemaining = loan->at(sfPaymentRemaining);
BEAST_EXPECT(paymentRemaining == 12); BEAST_EXPECT(paymentRemaining == 12);
assetsAvailable = loan->at(sfAssetsAvailable); assetsAvailable = loan->at(sfAssetsAvailable);
@@ -977,6 +993,8 @@ class Loan_test : public beast::unit_test::suite
principalOutstanding = loan->at(sfPrincipalOutstanding); principalOutstanding = loan->at(sfPrincipalOutstanding);
BEAST_EXPECT( BEAST_EXPECT(
principalOutstanding == broker.asset(1000).value()); principalOutstanding == broker.asset(1000).value());
paymentInterval = loan->at(sfPaymentInterval);
BEAST_EXPECT(paymentInterval == 600);
flags = loan->at(sfFlags); flags = loan->at(sfFlags);
BEAST_EXPECT(flags == baseFlag); BEAST_EXPECT(flags == baseFlag);
} }
@@ -1024,7 +1042,6 @@ class Loan_test : public beast::unit_test::suite
env(manage(lender, loanKeylet.key, tfLoanDefault)); env(manage(lender, loanKeylet.key, tfLoanDefault));
flags |= tfLoanDefault; flags |= tfLoanDefault;
flags &= ~tfLoanImpair;
paymentRemaining = 0; paymentRemaining = 0;
assetsAvailable = 0; assetsAvailable = 0;
principalOutstanding = 0; principalOutstanding = 0;
@@ -1058,6 +1075,17 @@ class Loan_test : public beast::unit_test::suite
tfLoanOverpayment, tfLoanOverpayment,
defaultBeforeStartDate(lsfLoanOverpayment)); defaultBeforeStartDate(lsfLoanOverpayment));
lifecycle(
"Loan overpayment prohibited - Default before start date",
env,
lender,
borrower,
evan,
broker,
pseudoAcct,
0,
defaultBeforeStartDate(0));
#if 0 #if 0
lifecycle( lifecycle(
"Loan overpayment allowed - Pay off", "Loan overpayment allowed - Pay off",
@@ -1072,24 +1100,13 @@ class Loan_test : public beast::unit_test::suite
VerifyLoanStatus const& verifyLoanStatus) { VerifyLoanStatus const& verifyLoanStatus) {
// toEndOfLife // toEndOfLife
// //
// TODO: Draw and make some payments
// Make payments down to 0 // Make payments down to 0
// TODO: Try to impair a paid off loan // TODO: Try to impair a paid off loan
}); });
#endif
lifecycle(
"Loan overpayment prohibited - Default before start date",
env,
lender,
borrower,
evan,
broker,
pseudoAcct,
0,
defaultBeforeStartDate(0));
#if 0
lifecycle( lifecycle(
"Loan overpayment prohibited - Pay off", "Loan overpayment prohibited - Pay off",
env, env,
@@ -1103,6 +1120,8 @@ class Loan_test : public beast::unit_test::suite
VerifyLoanStatus const& verifyLoanStatus) { VerifyLoanStatus const& verifyLoanStatus) {
// toEndOfLife // toEndOfLife
// //
// TODO: Draw and make some payments
// Make payments down to 0 // Make payments down to 0
// TODO: Try to impair a paid off loan // TODO: Try to impair a paid off loan

View File

@@ -0,0 +1,121 @@
//------------------------------------------------------------------------------
/*
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_APP_MISC_LENDINGHELPERS_H_INCLUDED
#define RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpl/basics/Number.h>
#include <xrpl/protocol/Feature.h>
namespace ripple {
class PreflightContext;
// Lending protocol has dependencies, so capture them here.
inline bool
LendingProtocolEnabled(PreflightContext const& ctx)
{
return ctx.rules.enabled(featureLendingProtocol) &&
VaultCreate::isEnabled(ctx);
}
inline Number
LoanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
{
// Need floating point math for this one, since we're dividing by some large
// numbers
return tenthBipsOfValue(Number(paymentInterval), interestRate) /
(365 * 24 * 60 * 60);
}
inline Number
LoanPeriodicPayment(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
if (principalOutstanding == 0 || paymentsRemaining == 0)
return 0;
Number const periodicRate = LoanPeriodicRate(interestRate, paymentInterval);
// TODO: Need a better name
Number const timeFactor = power(1 + periodicRate, paymentsRemaining);
return principalOutstanding * (periodicRate * timeFactor) /
(timeFactor - 1);
}
inline Number
LoanTotalValueOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return LoanPeriodicPayment(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining) *
paymentsRemaining;
}
inline Number
LoanTotalInterestOutstanding(
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining)
{
return LoanTotalValueOutstanding(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining) -
principalOutstanding;
}
template <AssetType A>
Number
LoanInterestOutstandingToVault(
A const& asset,
Number principalOutstanding,
TenthBips32 interestRate,
std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate)
{
return roundToAsset(
asset,
tenthBipsOfValue(
LoanTotalInterestOutstanding(
principalOutstanding,
interestRate,
paymentInterval,
paymentsRemaining),
tenthBipsPerUnity - managementFeeRate));
}
} // namespace ripple
#endif // RIPPLE_APP_MISC_LENDINGHELPERS_H_INCLUDED

View File

@@ -0,0 +1,29 @@
//------------------------------------------------------------------------------
/*
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 <xrpld/app/misc/LendingHelpers.h>
//
#include <xrpld/app/tx/detail/Transactor.h>
#include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpl/protocol/Feature.h>
namespace ripple {
} // namespace ripple

View File

@@ -18,7 +18,8 @@
//============================================================================== //==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h> #include <xrpld/app/tx/detail/LoanBrokerCoverDeposit.h>
#include <xrpld/app/tx/detail/LoanBrokerSet.h> //
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h> #include <xrpld/ledger/View.h>
@@ -44,7 +45,7 @@ namespace ripple {
bool bool
LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx) LoanBrokerCoverDeposit::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t

View File

@@ -18,7 +18,8 @@
//============================================================================== //==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h> #include <xrpld/app/tx/detail/LoanBrokerCoverWithdraw.h>
#include <xrpld/app/tx/detail/LoanBrokerSet.h> //
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h> #include <xrpld/ledger/View.h>
@@ -44,7 +45,7 @@ namespace ripple {
bool bool
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx) LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t
@@ -122,7 +123,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (accountHolds( if (accountHolds(
ctx.view, ctx.view,
account, pseudoAccountID,
vaultAsset, vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN, FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED, AuthHandling::ahZERO_IF_UNAUTHORIZED,

View File

@@ -18,7 +18,8 @@
//============================================================================== //==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerDelete.h> #include <xrpld/app/tx/detail/LoanBrokerDelete.h>
#include <xrpld/app/tx/detail/LoanBrokerSet.h> //
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h> #include <xrpld/ledger/View.h>
@@ -44,7 +45,7 @@ namespace ripple {
bool bool
LoanBrokerDelete::isEnabled(PreflightContext const& ctx) LoanBrokerDelete::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t
@@ -168,6 +169,14 @@ LoanBrokerDelete::doApply()
view().erase(broker); view().erase(broker);
{
auto owner = view().peek(keylet::account(account_));
if (!owner)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
adjustOwnerCount(view(), owner, -2, j_);
}
return tesSUCCESS; return tesSUCCESS;
} }

View File

@@ -18,6 +18,8 @@
//============================================================================== //==============================================================================
#include <xrpld/app/tx/detail/LoanBrokerSet.h> #include <xrpld/app/tx/detail/LoanBrokerSet.h>
//
#include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/SignerEntries.h> #include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/VaultCreate.h> #include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
@@ -43,17 +45,10 @@
namespace ripple { namespace ripple {
bool
lendingProtocolEnabled(PreflightContext const& ctx)
{
return ctx.rules.enabled(featureLendingProtocol) &&
VaultCreate::isEnabled(ctx);
}
bool bool
LoanBrokerSet::isEnabled(PreflightContext const& ctx) LoanBrokerSet::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t
@@ -92,15 +87,6 @@ LoanBrokerSet::doPreflight(PreflightContext const& ctx)
return tesSUCCESS; return tesSUCCESS;
} }
XRPAmount
LoanBrokerSet::calculateBaseFee(ReadView const& view, STTx const& tx)
{
// One reserve increment is typically much greater than one base fee.
if (!tx.isFieldPresent(sfLoanBrokerID))
return calculateOwnerReserveFee(view, tx);
return Transactor::calculateBaseFee(view, tx);
}
TER TER
LoanBrokerSet::preclaim(PreclaimContext const& ctx) LoanBrokerSet::preclaim(PreclaimContext const& ctx)
{ {
@@ -186,14 +172,10 @@ LoanBrokerSet::doApply()
if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode)) if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode))
return ter; return ter;
/* We're already charging a higher fee, so we probably don't want to adjustOwnerCount(view, owner, 2, j_);
also charge a reserve. auto const ownerCount = owner->at(sfOwnerCount);
*
adjustOwnerCount(view, owner, 1, j_);
auto ownerCount = owner->at(sfOwnerCount);
if (mPriorBalance < view.fees().accountReserve(ownerCount)) if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE; return tecINSUFFICIENT_RESERVE;
*/
auto maybePseudo = auto maybePseudo =
createPseudoAccount(view, broker->key(), sfLoanBrokerID); createPseudoAccount(view, broker->key(), sfLoanBrokerID);

View File

@@ -24,10 +24,6 @@
namespace ripple { namespace ripple {
// Lending protocol has dependencies, so capture them here.
bool
lendingProtocolEnabled(PreflightContext const& ctx);
class LoanBrokerSet : public Transactor class LoanBrokerSet : public Transactor
{ {
public: public:
@@ -46,9 +42,6 @@ public:
static NotTEC static NotTEC
doPreflight(PreflightContext const& ctx); doPreflight(PreflightContext const& ctx);
static XRPAmount
calculateBaseFee(ReadView const& view, STTx const& tx);
static TER static TER
preclaim(PreclaimContext const& ctx); preclaim(PreclaimContext const& ctx);

View File

@@ -19,7 +19,7 @@
#include <xrpld/app/tx/detail/LoanDelete.h> #include <xrpld/app/tx/detail/LoanDelete.h>
// //
#include <xrpld/app/tx/detail/LoanBrokerSet.h> #include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h> #include <xrpld/ledger/View.h>
@@ -47,7 +47,7 @@ namespace ripple {
bool bool
LoanDelete::isEnabled(PreflightContext const& ctx) LoanDelete::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t
@@ -110,6 +110,9 @@ LoanDelete::doApply()
if (!loanSle) if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const borrower = loanSle->at(sfBorrower); auto const borrower = loanSle->at(sfBorrower);
auto const borrowerSle = view.peek(keylet::account(borrower));
if (!borrowerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerID = loanSle->at(sfLoanBrokerID); auto const brokerID = loanSle->at(sfLoanBrokerID);
auto const brokerSle = view.peek(keylet::loanbroker(brokerID)); auto const brokerSle = view.peek(keylet::loanbroker(brokerID));
@@ -156,6 +159,8 @@ LoanDelete::doApply()
// Decrement the LoanBroker's owner count. // Decrement the LoanBroker's owner count.
adjustOwnerCount(view, brokerSle, -1, j_); adjustOwnerCount(view, brokerSle, -1, j_);
// Decrement the borrower's owner count
adjustOwnerCount(view, borrowerSle, -1, j_);
return tesSUCCESS; return tesSUCCESS;
} }

View File

@@ -19,7 +19,7 @@
#include <xrpld/app/tx/detail/LoanManage.h> #include <xrpld/app/tx/detail/LoanManage.h>
// //
#include <xrpld/app/tx/detail/LoanBrokerSet.h> #include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/LoanSet.h> #include <xrpld/app/tx/detail/LoanSet.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
#include <xrpld/ledger/View.h> #include <xrpld/ledger/View.h>
@@ -48,7 +48,7 @@ namespace ripple {
bool bool
LoanManage::isEnabled(PreflightContext const& ctx) LoanManage::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t
@@ -99,6 +99,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
// Impairment only allows certain transitions. // Impairment only allows certain transitions.
// 1. Once it's in default, it can't be changed. // 1. Once it's in default, it can't be changed.
// 2. It can get worse: unimpaired -> impaired -> default // 2. It can get worse: unimpaired -> impaired -> default
// or unimpaired -> default
// 3. It can get better: impaired -> unimpaired // 3. It can get better: impaired -> unimpaired
// 4. If it's in a state, it can't be put in that state again. // 4. If it's in a state, it can't be put in that state again.
if (loanSle->isFlag(lsfLoanDefault)) if (loanSle->isFlag(lsfLoanDefault))
@@ -115,10 +116,10 @@ LoanManage::preclaim(PreclaimContext const& ctx)
} }
if (!(loanSle->isFlag(lsfLoanImpaired) || if (!(loanSle->isFlag(lsfLoanImpaired) ||
loanSle->isFlag(lsfLoanDefault)) && loanSle->isFlag(lsfLoanDefault)) &&
(tx.isFlag(tfLoanDefault) || tx.isFlag(tfLoanUnimpair))) (tx.isFlag(tfLoanUnimpair)))
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
<< "Loan is unimpaired. Only valid modification is to impair"; << "Loan is unimpaired. Can not be unimpaired again.";
return tecNO_PERMISSION; return tecNO_PERMISSION;
} }
if (loanSle->at(sfPaymentRemaining) == 0) if (loanSle->at(sfPaymentRemaining) == 0)
@@ -133,8 +134,7 @@ LoanManage::preclaim(PreclaimContext const& ctx)
loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod))) loanSle->at(sfNextPaymentDueDate) + loanSle->at(sfGracePeriod)))
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
<< "Loan is not in default. A loan can not be defaulted before the " << "A loan can not be defaulted before the next payment due date.";
"next payment due date.";
return tecTOO_SOON; return tecTOO_SOON;
} }
@@ -163,19 +163,20 @@ defaultLoan(
SLE::ref vaultSle, SLE::ref vaultSle,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& interestOutstanding, Number const& interestOutstanding,
std::uint32_t paymentInterval,
Asset const& vaultAsset, Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
// Calculate the amount of the Default that First-Loss Capital covers: // Calculate the amount of the Default that First-Loss Capital covers:
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto debtTotalProxy = brokerSle->at(sfDebtTotal); auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
auto const totalDefaultAmount = principalOutstanding + interestOutstanding; auto const totalDefaultAmount = principalOutstanding + interestOutstanding;
// The default Amount equals the outstanding principal and interest, // The default Amount equals the outstanding principal and interest,
// excluding any funds unclaimed by the Borrower. // excluding any funds unclaimed by the Borrower.
auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable); auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable);
auto const defaultAmount = totalDefaultAmount - assetsAvailableProxy; auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy;
// Apply the First-Loss Capital to the Default Amount // Apply the First-Loss Capital to the Default Amount
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
TenthBips32 const coverRateLiquidation{ TenthBips32 const coverRateLiquidation{
@@ -184,71 +185,78 @@ defaultLoan(
vaultAsset, vaultAsset,
std::min( std::min(
tenthBipsOfValue( tenthBipsOfValue(
tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), tenthBipsOfValue(
brokerDebtTotalProxy.value(), coverRateMinimum),
coverRateLiquidation), coverRateLiquidation),
defaultAmount)); defaultAmount));
if (STAmount{vaultAsset, defaultCovered} != defaultCovered) auto const returnToVault = defaultCovered + loanAssetsAvailableProxy;
{
JLOG(j.warn())
<< "LoanManage: defaultCovered amount is not a valid amount";
return tefBAD_LEDGER;
}
auto const defaultReturned = defaultCovered + assetsAvailableProxy;
auto const vaultDefaultAmount = defaultAmount - defaultCovered; auto const vaultDefaultAmount = defaultAmount - defaultCovered;
// Update the LoanBroker object:
// Decrease the Debt of the LoanBroker:
if (debtTotalProxy < totalDefaultAmount)
{
JLOG(j.warn())
<< "LoanBroker debt total is less than the default amount";
return tefBAD_LEDGER;
}
debtTotalProxy -= totalDefaultAmount;
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
{
JLOG(j.warn())
<< "LoanBroker cover available is less than amount covered";
return tefBAD_LEDGER;
}
coverAvailableProxy -= defaultCovered;
view.update(brokerSle);
// Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
loanSle->clearFlag(lsfLoanImpaired);
loanSle->at(sfPaymentRemaining) = 0;
assetsAvailableProxy = 0;
loanSle->at(sfPrincipalOutstanding) = 0;
view.update(loanSle);
// Update the Vault object: // Update the Vault object:
// Decrease the Total Value of the Vault:
auto vaultAssetsTotalProxy = vaultSle->at(sfAssetsTotal);
if (vaultAssetsTotalProxy < vaultDefaultAmount)
{ {
JLOG(j.warn()) // Decrease the Total Value of the Vault:
<< "Vault total assets is less than the vault default amount"; auto vaultAssetsTotalProxy = vaultSle->at(sfAssetsTotal);
return tefBAD_LEDGER; if (vaultAssetsTotalProxy < vaultDefaultAmount)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "Vault total assets is less than the vault default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
vaultAssetsTotalProxy -= vaultDefaultAmount;
// Increase the Asset Available of the Vault by liquidated First-Loss
// Capital and any unclaimed funds amount:
vaultSle->at(sfAssetsAvailable) += returnToVault;
// The loss has been realized
if (loanSle->isFlag(lsfLoanImpaired))
{
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
if (vaultLossUnrealizedProxy < totalDefaultAmount)
{
JLOG(j.warn())
<< "Vault unrealized loss is less than the default amount";
return tefBAD_LEDGER;
}
vaultLossUnrealizedProxy -= totalDefaultAmount;
}
view.update(vaultSle);
} }
vaultAssetsTotalProxy -= vaultDefaultAmount;
// Increase the Asset Available of the Vault by liquidated First-Loss // Update the LoanBroker object:
// Capital and any unclaimed funds amount:
vaultSle->at(sfAssetsAvailable) += defaultReturned;
// The loss has been realized
auto vaultLossUnrealizedProxy = vaultSle->at(sfLossUnrealized);
if (vaultLossUnrealizedProxy < totalDefaultAmount)
{ {
JLOG(j.warn()) // Decrease the Debt of the LoanBroker:
<< "Vault unrealized loss is less than the default amount"; if (brokerDebtTotalProxy < totalDefaultAmount)
return tefBAD_LEDGER; {
// LCOV_EXCL_START
JLOG(j.warn())
<< "LoanBroker debt total is less than the default amount";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
brokerDebtTotalProxy -= totalDefaultAmount;
// Decrease the First-Loss Capital Cover Available:
auto coverAvailableProxy = brokerSle->at(sfCoverAvailable);
if (coverAvailableProxy < defaultCovered)
{
// LCOV_EXCL_START
JLOG(j.warn())
<< "LoanBroker cover available is less than amount covered";
return tefBAD_LEDGER;
// LCOV_EXCL_STOP
}
coverAvailableProxy -= defaultCovered;
view.update(brokerSle);
} }
vaultLossUnrealizedProxy -= totalDefaultAmount;
view.update(vaultSle); // Update the Loan object:
loanSle->setFlag(lsfLoanDefault);
loanSle->at(sfPaymentRemaining) = 0;
loanAssetsAvailableProxy = 0;
loanSle->at(sfPrincipalOutstanding) = 0;
view.update(loanSle);
// Return funds from the LoanBroker pseudo-account to the // Return funds from the LoanBroker pseudo-account to the
// Vault pseudo-account: // Vault pseudo-account:
@@ -256,7 +264,7 @@ defaultLoan(
view, view,
brokerSle->at(sfAccount), brokerSle->at(sfAccount),
vaultSle->at(sfAccount), vaultSle->at(sfAccount),
STAmount{vaultAsset, defaultReturned}, STAmount{vaultAsset, returnToVault},
j, j,
WaiveTransferFee::Yes); WaiveTransferFee::Yes);
} }
@@ -269,6 +277,7 @@ impairLoan(
SLE::ref vaultSle, SLE::ref vaultSle,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& interestOutstanding, Number const& interestOutstanding,
std::uint32_t paymentInterval,
Asset const& vaultAsset, Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
@@ -279,12 +288,12 @@ impairLoan(
// Update the Loan object // Update the Loan object
loanSle->setFlag(lsfLoanImpaired); loanSle->setFlag(lsfLoanImpaired);
auto nextDueProxy = loanSle->at(sfNextPaymentDueDate); auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!hasExpired(view, nextDueProxy)) if (!hasExpired(view, loanNextDueProxy))
{ {
// loan payment is not yet late - // loan payment is not yet late -
// move the next payment due date to now // move the next payment due date to now
nextDueProxy = view.parentCloseTime().time_since_epoch().count(); loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
} }
view.update(loanSle); view.update(loanSle);
@@ -299,6 +308,7 @@ unimpairLoan(
SLE::ref vaultSle, SLE::ref vaultSle,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& interestOutstanding, Number const& interestOutstanding,
std::uint32_t paymentInterval,
Asset const& vaultAsset, Asset const& vaultAsset,
beast::Journal j) beast::Journal j)
{ {
@@ -307,18 +317,20 @@ unimpairLoan(
auto const lossReversed = principalOutstanding + interestOutstanding; auto const lossReversed = principalOutstanding + interestOutstanding;
if (vaultLossUnrealizedProxy < lossReversed) if (vaultLossUnrealizedProxy < lossReversed)
{ {
// LCOV_EXCL_START
JLOG(j.warn()) JLOG(j.warn())
<< "Vault unrealized loss is less than the amount to be cleared"; << "Vault unrealized loss is less than the amount to be cleared";
return tefBAD_LEDGER; return tefBAD_LEDGER;
// LCOV_EXCL_STOP
} }
vaultLossUnrealizedProxy -= lossReversed; vaultLossUnrealizedProxy -= lossReversed;
view.update(vaultSle); view.update(vaultSle);
// Update the Loan object // Update the Loan object
loanSle->clearFlag(lsfLoanImpaired); loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate = auto const normalPaymentDueDate =
loanSle->at(sfPreviousPaymentDate) + paymentInterval; std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) +
paymentInterval;
if (!hasExpired(view, normalPaymentDueDate)) if (!hasExpired(view, normalPaymentDueDate))
{ {
// loan was unimpaired within the payment interval // loan was unimpaired within the payment interval
@@ -360,10 +372,14 @@ LoanManage::doApply()
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const interestOutstanding = LoanInterestOutstanding( auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
auto const interestOutstanding = LoanInterestOutstandingToVault(
vaultAsset, vaultAsset,
principalOutstanding.value(), principalOutstanding.value(),
interestRate, interestRate,
paymentInterval,
paymentsRemaining,
managementFeeRate); managementFeeRate);
// Valid flag combinations are checked in preflight. No flags is valid - // Valid flag combinations are checked in preflight. No flags is valid -
@@ -377,6 +393,7 @@ LoanManage::doApply()
vaultSle, vaultSle,
principalOutstanding, principalOutstanding,
interestOutstanding, interestOutstanding,
paymentInterval,
vaultAsset, vaultAsset,
j_)) j_))
return ter; return ter;
@@ -390,6 +407,7 @@ LoanManage::doApply()
vaultSle, vaultSle,
principalOutstanding, principalOutstanding,
interestOutstanding, interestOutstanding,
paymentInterval,
vaultAsset, vaultAsset,
j_)) j_))
return ter; return ter;
@@ -403,6 +421,7 @@ LoanManage::doApply()
vaultSle, vaultSle,
principalOutstanding, principalOutstanding,
interestOutstanding, interestOutstanding,
paymentInterval,
vaultAsset, vaultAsset,
j_)) j_))
return ter; return ter;

View File

@@ -19,7 +19,7 @@
#include <xrpld/app/tx/detail/LoanSet.h> #include <xrpld/app/tx/detail/LoanSet.h>
// //
#include <xrpld/app/tx/detail/LoanBrokerSet.h> #include <xrpld/app/misc/LendingHelpers.h>
#include <xrpld/app/tx/detail/SignerEntries.h> #include <xrpld/app/tx/detail/SignerEntries.h>
#include <xrpld/app/tx/detail/VaultCreate.h> #include <xrpld/app/tx/detail/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h> #include <xrpld/ledger/ApplyView.h>
@@ -49,7 +49,7 @@ namespace ripple {
bool bool
LoanSet::isEnabled(PreflightContext const& ctx) LoanSet::isEnabled(PreflightContext const& ctx)
{ {
return lendingProtocolEnabled(ctx); return LendingProtocolEnabled(ctx);
} }
std::uint32_t std::uint32_t
@@ -199,7 +199,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
if (!vault) if (!vault)
// Should be impossible // Should be impossible
return tefBAD_LEDGER; // LCOV_EXCL_LINE return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const asset = vault->at(sfAsset); Asset const asset = vault->at(sfAsset);
if (auto const ter = canAddHolding(ctx.view, asset)) if (auto const ter = canAddHolding(ctx.view, asset))
return ter; return ter;
@@ -215,6 +215,8 @@ LoanSet::preclaim(PreclaimContext const& ctx)
auto const issue = asset.get<Issue>(); auto const issue = asset.get<Issue>();
if (isDeepFrozen(ctx.view, borrower, issue.currency, issue.account)) if (isDeepFrozen(ctx.view, borrower, issue.currency, issue.account))
return tecFROZEN; return tecFROZEN;
if (isDeepFrozen(ctx.view, brokerPseudo, issue.currency, issue.account))
return tecFROZEN;
} }
auto const principalRequested = tx[sfPrincipalRequested]; auto const principalRequested = tx[sfPrincipalRequested];
@@ -225,23 +227,52 @@ LoanSet::preclaim(PreclaimContext const& ctx)
<< "Insufficient assets available in the Vault to fund the loan."; << "Insufficient assets available in the Vault to fund the loan.";
return tecINSUFFICIENT_FUNDS; return tecINSUFFICIENT_FUNDS;
} }
auto const debtTotal = brokerSle->at(sfDebtTotal); auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested;
if (brokerSle->at(sfDebtMaximum) < debtTotal + principalRequested) if (brokerSle->at(sfDebtMaximum) < newDebtTotal)
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
<< "Loan would exceed the maximum debt limit of the LoanBroker."; << "Loan would exceed the maximum debt limit of the LoanBroker.";
return tecLIMIT_EXCEEDED; return tecLIMIT_EXCEEDED;
} }
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
if (brokerSle->at(sfCoverAvailable) < if (brokerSle->at(sfCoverAvailable) <
tenthBipsOfValue( tenthBipsOfValue(newDebtTotal, coverRateMinimum))
debtTotal + principalRequested,
TenthBips32(brokerSle->at(sfCoverRateMinimum))))
{ {
JLOG(ctx.j.warn()) JLOG(ctx.j.warn())
<< "Insufficient first-loss capital to cover the loan."; << "Insufficient first-loss capital to cover the loan.";
return tecINSUFFICIENT_FUNDS; return tecINSUFFICIENT_FUNDS;
} }
// Check that the lender will not make a profit on the lending fee if the
// loan defaults. (Not yet in spec. May not be included.)
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
TenthBips32 const coverRateLiquidation{
brokerSle->at(sfCoverRateLiquidation)};
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto const totalInterestToVault = LoanInterestOutstandingToVault(
asset,
principalRequested,
interestRate,
paymentInterval,
paymentTotal,
managementFeeRate);
auto const maximumOriginationFee = tenthBipsOfValue(
tenthBipsOfValue(newDebtTotal, coverRateMinimum), coverRateLiquidation);
if (auto const originationFee = tx[~sfLoanOriginationFee];
originationFee && *originationFee > maximumOriginationFee)
{
JLOG(ctx.j.warn())
<< "Loan origination fee is too high. The lender will make a "
"profit on the lending fee if the loan defaults.";
return tecINSUFFICIENT_FUNDS;
}
return tesSUCCESS; return tesSUCCESS;
} }
@@ -258,12 +289,14 @@ LoanSet::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner); auto const brokerOwner = brokerSle->at(sfOwner);
auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner)); auto const brokerOwnerSle = view.peek(keylet::account(brokerOwner));
if (!brokerOwnerSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID)));
if (!vaultSle) if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const vaultPseudo = vaultSle->at(sfAccount); auto const vaultPseudo = vaultSle->at(sfAccount);
auto const vaultAsset = vaultSle->at(sfAsset); Asset const vaultAsset = vaultSle->at(sfAsset);
auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner); auto const counterparty = tx[~sfCounterparty].value_or(brokerOwner);
auto const borrower = counterparty == brokerOwner ? account_ : counterparty; auto const borrower = counterparty == brokerOwner ? account_ : counterparty;
@@ -275,6 +308,10 @@ LoanSet::doApply()
auto const brokerPseudo = brokerSle->at(sfAccount); auto const brokerPseudo = brokerSle->at(sfAccount);
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo)); auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
if (!brokerPseudoSle)
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const principalRequested = tx[sfPrincipalRequested]; auto const principalRequested = tx[sfPrincipalRequested];
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const originationFee = tx[~sfLoanOriginationFee]; auto const originationFee = tx[~sfLoanOriginationFee];
@@ -292,19 +329,6 @@ LoanSet::doApply()
// //
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee) // 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
// from vault pseudo-account to LoanBroker pseudo-account. // from vault pseudo-account to LoanBroker pseudo-account.
//
// Create the holding if it doesn't already exist (necessary for MPTs)
if (auto const ter = addEmptyHolding(
view,
brokerPseudo,
brokerPseudoSle->at(sfBalance).value().xrp(),
vaultAsset,
j_);
!isTesSuccess(ter) && ter != tecDUPLICATE)
// ignore tecDUPLICATE. That means the holding already exists, and is
// fine here
return ter;
// 1a. Transfer the loanAssetsAvailable to the pseudo-account
if (auto const ter = accountSend( if (auto const ter = accountSend(
view, view,
vaultPseudo, vaultPseudo,
@@ -317,7 +341,8 @@ LoanSet::doApply()
// LoanBroker owner. // LoanBroker owner.
if (originationFee) if (originationFee)
{ {
// Create the holding if it doesn't already exist (necessary for MPTs) // Create the holding if it doesn't already exist (necessary for MPTs).
// The owner may have deleted their MPT / line at some point.
if (auto const ter = addEmptyHolding( if (auto const ter = addEmptyHolding(
view, view,
brokerOwner, brokerOwner,
@@ -338,23 +363,32 @@ LoanSet::doApply()
return ter; return ter;
} }
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
// The portion of the loan interest that will go to the vault (total // The portion of the loan interest that will go to the vault (total
// interest minus the management fee) // interest minus the management fee)
auto const loanInterestToVault = LoanInterestOutstanding( auto const loanInterestToVault = LoanInterestOutstandingToVault(
vaultAsset, principalRequested, interestRate, managementFeeRate); vaultAsset,
principalRequested,
interestRate,
paymentInterval,
paymentTotal,
managementFeeRate);
auto const startDate = tx[sfStartDate]; auto const startDate = tx[sfStartDate];
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto loanSequence = brokerSle->at(sfLoanSequence); auto loanSequence = brokerSle->at(sfLoanSequence);
// Create the loan // Create the loan
auto loan = std::make_shared<SLE>(keylet::loan(brokerID, *loanSequence)); auto loan = std::make_shared<SLE>(keylet::loan(brokerID, *loanSequence));
// Prevent copy/paste errors // Prevent copy/paste errors
auto setLoanField = [&loan, &tx](auto const& field) { auto setLoanField =
loan->at(field) = tx[field].value_or(0); [&loan, &tx](auto const& field, std::uint32_t const defValue = 0) {
}; // at() is smart enough to unseat a default field set to the default
// value
loan->at(field) = tx[field].value_or(defValue);
};
// Set required tx fields and pre-computed fields // Set required tx fields and pre-computed fields
loan->at(sfPrincipalOutstanding) = principalRequested; loan->at(sfPrincipalOutstanding) = principalRequested;
@@ -375,12 +409,11 @@ LoanSet::doApply()
setLoanField(~sfLateInterestRate); setLoanField(~sfLateInterestRate);
setLoanField(~sfCloseInterestRate); setLoanField(~sfCloseInterestRate);
setLoanField(~sfOverpaymentInterestRate); setLoanField(~sfOverpaymentInterestRate);
loan->at(sfGracePeriod) = tx[~sfGracePeriod].value_or(defaultGracePeriod); setLoanField(~sfGracePeriod, defaultGracePeriod);
// Set dynamic fields to their initial values // Set dynamic fields to their initial values
loan->at(sfPreviousPaymentDate) = 0; loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = loan->at(sfPaymentRemaining) = paymentTotal;
tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
loan->at(sfAssetsAvailable) = loanAssetsAvailable; loan->at(sfAssetsAvailable) = loanAssetsAvailable;
loan->at(sfPrincipalOutstanding) = principalRequested; loan->at(sfPrincipalOutstanding) = principalRequested;
view.insert(loan); view.insert(loan);

View File

@@ -24,21 +24,6 @@
namespace ripple { namespace ripple {
template <AssetType A>
Number
LoanInterestOutstanding(
A const& asset,
Number principalOutstanding,
TenthBips32 interestRate,
TenthBips32 managementFeeRate)
{
return roundToAsset(
asset,
tenthBipsOfValue(
tenthBipsOfValue(principalOutstanding, interestRate),
tenthBipsPerUnity - managementFeeRate));
}
class LoanSet : public Transactor class LoanSet : public Transactor
{ {
public: public: