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;
// 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 =
keylet::loanbroker(alice.id(), env.seq(alice));
// Other LoanBroker transactions are disabled, too.
@@ -129,7 +129,7 @@ class LoanBroker_test : public beast::unit_test::suite
{
// 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
if (modifyJTx)
jtx = modifyJTx(jtx);
@@ -390,6 +390,8 @@ class LoanBroker_test : public beast::unit_test::suite
env.close();
}
auto const aliceOriginalCount = env.ownerCount(alice);
// Create and update Loan Brokers
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));
// Try some failure cases
// insufficient fee
env(set(evan, vault.vaultID), ter(telINSUF_FEE_P));
// not the vault owner
env(set(evan, vault.vaultID),
fee(increment),
ter(tecNO_PERMISSION));
env(set(evan, vault.vaultID), ter(tecNO_PERMISSION));
// 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
env(set(evan, vault.vaultID, ~tfUniversal),
fee(increment),
ter(temINVALID_FLAG));
env(set(evan, vault.vaultID, ~tfUniversal), ter(temINVALID_FLAG));
// field length validation
// sfData: good length, bad account
env(set(evan, vault.vaultID),
fee(increment),
data(std::string(maxDataPayloadLength, 'X')),
ter(tecNO_PERMISSION));
// sfData: too long
env(set(evan, vault.vaultID),
fee(increment),
data(std::string(maxDataPayloadLength + 1, 'Y')),
ter(temINVALID));
// sfManagementFeeRate: good value, bad account
env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate),
fee(increment),
ter(tecNO_PERMISSION));
// sfManagementFeeRate: too big
env(set(evan, vault.vaultID),
managementFeeRate(maxManagementFeeRate + TenthBips16(10)),
fee(increment),
ter(temINVALID));
// sfCoverRateMinimum: good value, bad account
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate),
fee(increment),
ter(tecNO_PERMISSION));
// sfCoverRateMinimum: too big
env(set(evan, vault.vaultID),
coverRateMinimum(maxCoverRate + 1),
fee(increment),
ter(temINVALID));
// sfCoverRateLiquidation: good value, bad account
env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate),
fee(increment),
ter(tecNO_PERMISSION));
// sfCoverRateLiquidation: too big
env(set(evan, vault.vaultID),
coverRateLiquidation(maxCoverRate + 1),
fee(increment),
ter(temINVALID));
// sfDebtMaximum: good value, bad account
env(set(evan, vault.vaultID),
debtMaximum(Number(0)),
fee(increment),
ter(tecNO_PERMISSION));
// sfDebtMaximum: overflow
env(set(evan, vault.vaultID),
debtMaximum(Number(1, 100)),
fee(increment),
ter(temINVALID));
// sfDebtMaximum: negative
env(set(evan, vault.vaultID),
debtMaximum(Number(-1)),
fee(increment),
ter(temINVALID));
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(sfCoverRateMinimum) == 0);
BEAST_EXPECT(broker->at(sfCoverRateLiquidation) == 0);
BEAST_EXPECT(
env.ownerCount(alice) == aliceOriginalCount + 2);
},
[&](SLE::const_ref broker) {
// Modifications
@@ -587,6 +575,8 @@ class LoanBroker_test : public beast::unit_test::suite
BEAST_EXPECT(!broker->isFieldPresent(sfDebtMaximum));
});
}
BEAST_EXPECT(env.ownerCount(alice) == aliceOriginalCount);
}
public:

View File

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

View File

@@ -18,7 +18,8 @@
//==============================================================================
#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/View.h>
@@ -44,7 +45,7 @@ namespace ripple {
bool
LoanBrokerCoverWithdraw::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
return LendingProtocolEnabled(ctx);
}
std::uint32_t
@@ -122,7 +123,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
if (accountHolds(
ctx.view,
account,
pseudoAccountID,
vaultAsset,
FreezeHandling::fhZERO_IF_FROZEN,
AuthHandling::ahZERO_IF_UNAUTHORIZED,

View File

@@ -18,7 +18,8 @@
//==============================================================================
#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/View.h>
@@ -44,7 +45,7 @@ namespace ripple {
bool
LoanBrokerDelete::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
return LendingProtocolEnabled(ctx);
}
std::uint32_t
@@ -168,6 +169,14 @@ LoanBrokerDelete::doApply()
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;
}

View File

@@ -18,6 +18,8 @@
//==============================================================================
#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/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h>
@@ -43,17 +45,10 @@
namespace ripple {
bool
lendingProtocolEnabled(PreflightContext const& ctx)
{
return ctx.rules.enabled(featureLendingProtocol) &&
VaultCreate::isEnabled(ctx);
}
bool
LoanBrokerSet::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
return LendingProtocolEnabled(ctx);
}
std::uint32_t
@@ -92,15 +87,6 @@ LoanBrokerSet::doPreflight(PreflightContext const& ctx)
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
LoanBrokerSet::preclaim(PreclaimContext const& ctx)
{
@@ -186,14 +172,10 @@ LoanBrokerSet::doApply()
if (auto const ter = dirLink(view, vaultPseudoID, broker, sfVaultNode))
return ter;
/* We're already charging a higher fee, so we probably don't want to
also charge a reserve.
*
adjustOwnerCount(view, owner, 1, j_);
auto ownerCount = owner->at(sfOwnerCount);
adjustOwnerCount(view, owner, 2, j_);
auto const ownerCount = owner->at(sfOwnerCount);
if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
*/
auto maybePseudo =
createPseudoAccount(view, broker->key(), sfLoanBrokerID);

View File

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

View File

@@ -19,7 +19,7 @@
#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/View.h>
@@ -47,7 +47,7 @@ namespace ripple {
bool
LoanDelete::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
return LendingProtocolEnabled(ctx);
}
std::uint32_t
@@ -110,6 +110,9 @@ LoanDelete::doApply()
if (!loanSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
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 brokerSle = view.peek(keylet::loanbroker(brokerID));
@@ -156,6 +159,8 @@ LoanDelete::doApply()
// Decrement the LoanBroker's owner count.
adjustOwnerCount(view, brokerSle, -1, j_);
// Decrement the borrower's owner count
adjustOwnerCount(view, borrowerSle, -1, j_);
return tesSUCCESS;
}

View File

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

View File

@@ -19,7 +19,7 @@
#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/VaultCreate.h>
#include <xrpld/ledger/ApplyView.h>
@@ -49,7 +49,7 @@ namespace ripple {
bool
LoanSet::isEnabled(PreflightContext const& ctx)
{
return lendingProtocolEnabled(ctx);
return LendingProtocolEnabled(ctx);
}
std::uint32_t
@@ -199,7 +199,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
if (!vault)
// Should be impossible
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))
return ter;
@@ -215,6 +215,8 @@ LoanSet::preclaim(PreclaimContext const& ctx)
auto const issue = asset.get<Issue>();
if (isDeepFrozen(ctx.view, borrower, issue.currency, issue.account))
return tecFROZEN;
if (isDeepFrozen(ctx.view, brokerPseudo, issue.currency, issue.account))
return tecFROZEN;
}
auto const principalRequested = tx[sfPrincipalRequested];
@@ -225,23 +227,52 @@ LoanSet::preclaim(PreclaimContext const& ctx)
<< "Insufficient assets available in the Vault to fund the loan.";
return tecINSUFFICIENT_FUNDS;
}
auto const debtTotal = brokerSle->at(sfDebtTotal);
if (brokerSle->at(sfDebtMaximum) < debtTotal + principalRequested)
auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested;
if (brokerSle->at(sfDebtMaximum) < newDebtTotal)
{
JLOG(ctx.j.warn())
<< "Loan would exceed the maximum debt limit of the LoanBroker.";
return tecLIMIT_EXCEEDED;
}
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
if (brokerSle->at(sfCoverAvailable) <
tenthBipsOfValue(
debtTotal + principalRequested,
TenthBips32(brokerSle->at(sfCoverRateMinimum))))
tenthBipsOfValue(newDebtTotal, coverRateMinimum))
{
JLOG(ctx.j.warn())
<< "Insufficient first-loss capital to cover the loan.";
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;
}
@@ -258,12 +289,14 @@ LoanSet::doApply()
return tefBAD_LEDGER; // LCOV_EXCL_LINE
auto const brokerOwner = brokerSle->at(sfOwner);
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)));
if (!vaultSle)
return tefBAD_LEDGER; // LCOV_EXCL_LINE
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 borrower = counterparty == brokerOwner ? account_ : counterparty;
@@ -275,6 +308,10 @@ LoanSet::doApply()
auto const brokerPseudo = brokerSle->at(sfAccount);
auto const brokerPseudoSle = view.peek(keylet::account(brokerPseudo));
if (!brokerPseudoSle)
{
return tefBAD_LEDGER; // LCOV_EXCL_LINE
}
auto const principalRequested = tx[sfPrincipalRequested];
TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const originationFee = tx[~sfLoanOriginationFee];
@@ -292,19 +329,6 @@ LoanSet::doApply()
//
// 1. Transfer loanAssetsAvailable (principalRequested - originationFee)
// 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(
view,
vaultPseudo,
@@ -317,7 +341,8 @@ LoanSet::doApply()
// LoanBroker owner.
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(
view,
brokerOwner,
@@ -338,23 +363,32 @@ LoanSet::doApply()
return ter;
}
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
// The portion of the loan interest that will go to the vault (total
// interest minus the management fee)
auto const loanInterestToVault = LoanInterestOutstanding(
vaultAsset, principalRequested, interestRate, managementFeeRate);
auto const loanInterestToVault = LoanInterestOutstandingToVault(
vaultAsset,
principalRequested,
interestRate,
paymentInterval,
paymentTotal,
managementFeeRate);
auto const startDate = tx[sfStartDate];
auto const paymentInterval =
tx[~sfPaymentInterval].value_or(defaultPaymentInterval);
auto loanSequence = brokerSle->at(sfLoanSequence);
// Create the loan
auto loan = std::make_shared<SLE>(keylet::loan(brokerID, *loanSequence));
// Prevent copy/paste errors
auto setLoanField = [&loan, &tx](auto const& field) {
loan->at(field) = tx[field].value_or(0);
};
auto setLoanField =
[&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
loan->at(sfPrincipalOutstanding) = principalRequested;
@@ -375,12 +409,11 @@ LoanSet::doApply()
setLoanField(~sfLateInterestRate);
setLoanField(~sfCloseInterestRate);
setLoanField(~sfOverpaymentInterestRate);
loan->at(sfGracePeriod) = tx[~sfGracePeriod].value_or(defaultGracePeriod);
setLoanField(~sfGracePeriod, defaultGracePeriod);
// Set dynamic fields to their initial values
loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) =
tx[~sfPaymentTotal].value_or(defaultPaymentTotal);
loan->at(sfPaymentRemaining) = paymentTotal;
loan->at(sfAssetsAvailable) = loanAssetsAvailable;
loan->at(sfPrincipalOutstanding) = principalRequested;
view.insert(loan);

View File

@@ -24,21 +24,6 @@
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
{
public: