mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-27 22:45:52 +00:00
Update to match latest spec: compute interest, LoanBroker reserves
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
121
src/xrpld/app/misc/LendingHelpers.h
Normal file
121
src/xrpld/app/misc/LendingHelpers.h
Normal 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
|
||||
29
src/xrpld/app/misc/detail/LendingHelpers.cpp
Normal file
29
src/xrpld/app/misc/detail/LendingHelpers.cpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user