mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-28 06:55:50 +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;
|
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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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/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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user