mirror of
https://github.com/XRPLF/rippled.git
synced 2026-07-29 18:10:34 +00:00
[WIP] Test single payments, does not pass tests, may not build
This commit is contained in:
@@ -1039,7 +1039,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
return state;
|
||||
};
|
||||
|
||||
auto defaultBeforeStartDate = [&](std::uint32_t baseFlag) {
|
||||
auto defaultBeforeStartDate = [&](std::uint32_t baseFlag,
|
||||
bool impair = true) {
|
||||
return [&, baseFlag](
|
||||
Keylet const& loanKeylet,
|
||||
VerifyLoanStatus const& verifyLoanStatus) {
|
||||
@@ -1051,17 +1052,20 @@ class Loan_test : public beast::unit_test::suite
|
||||
auto state = currentState(loanKeylet, verifyLoanStatus);
|
||||
BEAST_EXPECT(state.flags == baseFlag);
|
||||
|
||||
// Impair the loan
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair));
|
||||
if (impair)
|
||||
{
|
||||
// Impair the loan
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair));
|
||||
|
||||
state.flags |= tfLoanImpair;
|
||||
state.nextPaymentDate =
|
||||
env.now().time_since_epoch().count();
|
||||
verifyLoanStatus(state);
|
||||
state.flags |= tfLoanImpair;
|
||||
state.nextPaymentDate =
|
||||
env.now().time_since_epoch().count();
|
||||
verifyLoanStatus(state);
|
||||
|
||||
// Once the loan is impaired, it can't be impaired again
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair),
|
||||
ter(tecNO_PERMISSION));
|
||||
// Once the loan is impaired, it can't be impaired again
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair),
|
||||
ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
auto const nextDueDate = tp{d{state.nextPaymentDate}};
|
||||
|
||||
@@ -1074,9 +1078,12 @@ class Loan_test : public beast::unit_test::suite
|
||||
// defaulted
|
||||
env.close(nextDueDate + 60s);
|
||||
|
||||
// Impaired loans can't be drawn against
|
||||
env(draw(borrower, loanKeylet.key, broker.asset(100)),
|
||||
ter(tecNO_PERMISSION));
|
||||
if (impair)
|
||||
{
|
||||
// Impaired loans can't be drawn against
|
||||
env(draw(borrower, loanKeylet.key, broker.asset(100)),
|
||||
ter(tecNO_PERMISSION));
|
||||
}
|
||||
|
||||
// Default the loan
|
||||
env(manage(lender, loanKeylet.key, tfLoanDefault));
|
||||
@@ -1099,11 +1106,154 @@ class Loan_test : public beast::unit_test::suite
|
||||
};
|
||||
};
|
||||
|
||||
auto immediatePayoff = [&](std::uint32_t baseFlag) {
|
||||
return [&, baseFlag](
|
||||
Keylet const& loanKeylet,
|
||||
VerifyLoanStatus const& verifyLoanStatus) {
|
||||
// toEndOfLife
|
||||
//
|
||||
auto state = currentState(loanKeylet, verifyLoanStatus);
|
||||
BEAST_EXPECT(state.flags == baseFlag);
|
||||
auto const borrowerStartingBalance =
|
||||
env.balance(borrower, broker.asset);
|
||||
|
||||
// Try to make a payment before the loan starts
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(500)),
|
||||
ter(tecTOO_SOON));
|
||||
|
||||
// Advance to the start date of the loan
|
||||
env.close(state.startDate + 5s);
|
||||
|
||||
verifyLoanStatus(state);
|
||||
|
||||
// Need to account for fees if the loan is in XRP
|
||||
PrettyAmount adjustment = broker.asset(0);
|
||||
if (broker.asset.raw().native())
|
||||
{
|
||||
adjustment = 2 * env.current()->fees().base;
|
||||
}
|
||||
|
||||
// Draw the entire available balance
|
||||
// Need to create the STAmount directly to avoid
|
||||
// PrettyAsset scaling.
|
||||
STAmount const drawAmount{
|
||||
broker.asset, state.assetsAvailable};
|
||||
env(draw(borrower, loanKeylet.key, drawAmount));
|
||||
env.close(state.startDate + 20s);
|
||||
auto const loanAge = (env.now() - state.startDate).count();
|
||||
BEAST_EXPECT(loanAge == 30);
|
||||
|
||||
state.assetsAvailable -= drawAmount;
|
||||
verifyLoanStatus(state);
|
||||
BEAST_EXPECT(
|
||||
env.balance(borrower, broker.asset) ==
|
||||
borrowerStartingBalance + drawAmount - adjustment);
|
||||
|
||||
// Send some bogus pay transactions
|
||||
env(pay(borrower,
|
||||
keylet::loan(uint256(0)).key,
|
||||
broker.asset(10)),
|
||||
ter(temINVALID));
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(-100)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(pay(borrower, broker.brokerID, broker.asset(100)),
|
||||
ter(tecNO_ENTRY));
|
||||
env(pay(evan, loanKeylet.key, broker.asset(500)),
|
||||
ter(tecNO_PERMISSION));
|
||||
|
||||
{
|
||||
auto const otherAsset =
|
||||
broker.asset.raw() == assets[0].raw() ? assets[1]
|
||||
: assets[0];
|
||||
env(pay(borrower, loanKeylet.key, otherAsset(100)),
|
||||
ter(tecWRONG_ASSET));
|
||||
}
|
||||
|
||||
// Amount doesn't cover a single payment
|
||||
env(pay(borrower,
|
||||
loanKeylet.key,
|
||||
STAmount{broker.asset, 1}),
|
||||
ter(tecINSUFFICIENT_PAYMENT));
|
||||
|
||||
// Get the balance after these failed transactions take
|
||||
// fees
|
||||
auto const borrowerBalanceBeforePayment =
|
||||
env.balance(borrower, broker.asset);
|
||||
|
||||
// Full payoff amount will consist of
|
||||
// 1. principal outstanding (1000)
|
||||
// 2. accrued interest (at 12%)
|
||||
// 3. prepayment penalty (closeInterest at 3.6%)
|
||||
// 4. close payment fee (4)
|
||||
// Calculate these values without the helper functions
|
||||
// to verify they're working correctly The numbers in
|
||||
// the below BEAST_EXPECTs may not hold across assets.
|
||||
Number const interval = state.paymentInterval;
|
||||
auto const periodicRate =
|
||||
interval * Number(12, -2) / (365 * 24 * 60 * 60);
|
||||
BEAST_EXPECT(
|
||||
periodicRate ==
|
||||
Number(2283105022831050, -21, Number::unchecked{}));
|
||||
STAmount const accruedInterest{
|
||||
broker.asset,
|
||||
state.principalOutstanding * periodicRate * loanAge /
|
||||
interval};
|
||||
BEAST_EXPECT(
|
||||
accruedInterest ==
|
||||
broker.asset(Number(1141552511415525, -19)));
|
||||
STAmount const prepaymentPenalty{
|
||||
broker.asset,
|
||||
state.principalOutstanding * Number(36, -3)};
|
||||
BEAST_EXPECT(prepaymentPenalty == broker.asset(36));
|
||||
STAmount const closePaymentFee = broker.asset(4);
|
||||
auto const payoffAmount =
|
||||
STAmount{broker.asset, state.principalOutstanding} +
|
||||
accruedInterest + prepaymentPenalty + closePaymentFee;
|
||||
BEAST_EXPECT(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1040000114155251, -12)));
|
||||
BEAST_EXPECT(payoffAmount > drawAmount);
|
||||
// Try to pay a little extra to show that it's _not_
|
||||
// taken
|
||||
auto const transactionAmount =
|
||||
payoffAmount + broker.asset(10);
|
||||
BEAST_EXPECT(
|
||||
transactionAmount ==
|
||||
broker.asset(Number(1050000114155251, -12)));
|
||||
env(pay(borrower, loanKeylet.key, transactionAmount));
|
||||
|
||||
env.close();
|
||||
|
||||
// Need to account for fees if the loan is in XRP
|
||||
adjustment = broker.asset(0);
|
||||
if (broker.asset.raw().native())
|
||||
{
|
||||
adjustment = env.current()->fees().base;
|
||||
}
|
||||
|
||||
state.paymentRemaining = 0;
|
||||
state.principalOutstanding = 0;
|
||||
verifyLoanStatus(state);
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(borrower, broker.asset) ==
|
||||
borrowerBalanceBeforePayment - payoffAmount -
|
||||
adjustment);
|
||||
|
||||
// Can't impair or default a paid off loan
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair),
|
||||
ter(tecNO_PERMISSION));
|
||||
env(manage(lender, loanKeylet.key, tfLoanDefault),
|
||||
ter(tecNO_PERMISSION));
|
||||
};
|
||||
};
|
||||
|
||||
// There are a lot of fields that can be set on a loan, but most of
|
||||
// them only affect the "math" when a payment is made. The only one
|
||||
// that really affects behavior is the `tfLoanOverpayment` flag.
|
||||
lifecycle(
|
||||
"Loan overpayment allowed - Default before start date",
|
||||
"Loan overpayment allowed - Impair and Default before start "
|
||||
"date",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
@@ -1114,7 +1264,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
defaultBeforeStartDate(lsfLoanOverpayment));
|
||||
|
||||
lifecycle(
|
||||
"Loan overpayment prohibited - Default before start date",
|
||||
"Loan overpayment prohibited - Impair and Default before start "
|
||||
"date",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
@@ -1124,6 +1275,32 @@ class Loan_test : public beast::unit_test::suite
|
||||
0,
|
||||
defaultBeforeStartDate(0));
|
||||
|
||||
lifecycle(
|
||||
"Loan overpayment allowed - Default without Impair before "
|
||||
"start "
|
||||
"date",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
pseudoAcct,
|
||||
tfLoanOverpayment,
|
||||
defaultBeforeStartDate(lsfLoanOverpayment, false));
|
||||
|
||||
lifecycle(
|
||||
"Loan overpayment prohibited - Default without Impair before "
|
||||
"start "
|
||||
"date",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
pseudoAcct,
|
||||
0,
|
||||
defaultBeforeStartDate(0, false));
|
||||
|
||||
lifecycle(
|
||||
"Loan overpayment allowed - Draw then default",
|
||||
env,
|
||||
@@ -1224,34 +1401,53 @@ class Loan_test : public beast::unit_test::suite
|
||||
broker,
|
||||
pseudoAcct,
|
||||
0,
|
||||
immediatePayoff(0));
|
||||
|
||||
lifecycle(
|
||||
"Loan overpayment allowed - Pay off immediately",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
pseudoAcct,
|
||||
tfLoanOverpayment,
|
||||
immediatePayoff(lsfLoanOverpayment));
|
||||
|
||||
lifecycle(
|
||||
"Loan overpayment prohibited - Make payments",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
pseudoAcct,
|
||||
0,
|
||||
[&](Keylet const& loanKeylet,
|
||||
VerifyLoanStatus const& verifyLoanStatus) {
|
||||
// toEndOfLife
|
||||
//
|
||||
// Draw and make multiple payments
|
||||
auto state = currentState(loanKeylet, verifyLoanStatus);
|
||||
BEAST_EXPECT(state.flags == 0);
|
||||
auto const borrowerStartingBalance =
|
||||
env.balance(borrower, broker.asset);
|
||||
|
||||
// Try to make a payment before the loan starts
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(500)),
|
||||
ter(tecTOO_SOON));
|
||||
|
||||
// Advance to the start date of the loan
|
||||
env.close(state.startDate + 5s);
|
||||
|
||||
verifyLoanStatus(state);
|
||||
|
||||
auto const borrowerStartingBalance =
|
||||
env.balance(borrower, broker.asset);
|
||||
|
||||
// Need to account for fees if the loan is in XRP
|
||||
PrettyAmount adjustment = broker.asset(0);
|
||||
if (broker.asset.raw().native())
|
||||
{
|
||||
adjustment = 2 * env.current()->fees().base;
|
||||
adjustment = env.current()->fees().base;
|
||||
}
|
||||
|
||||
// Draw the entire available balance
|
||||
// Need to create the STAmount directly to avoid PrettyAsset
|
||||
// scaling.
|
||||
// Need to create the STAmount directly to avoid
|
||||
// PrettyAsset scaling.
|
||||
STAmount const drawAmount{
|
||||
broker.asset, state.assetsAvailable};
|
||||
env(draw(borrower, loanKeylet.key, drawAmount));
|
||||
@@ -1265,95 +1461,119 @@ class Loan_test : public beast::unit_test::suite
|
||||
env.balance(borrower, broker.asset) ==
|
||||
borrowerStartingBalance + drawAmount - adjustment);
|
||||
|
||||
// Send some bogus pay transactions
|
||||
env(pay(borrower,
|
||||
keylet::loan(uint256(0)).key,
|
||||
broker.asset(10)),
|
||||
ter(temINVALID));
|
||||
env(pay(borrower, loanKeylet.key, broker.asset(-100)),
|
||||
ter(temBAD_AMOUNT));
|
||||
env(pay(borrower, broker.brokerID, broker.asset(100)),
|
||||
ter(tecNO_ENTRY));
|
||||
env(pay(evan, loanKeylet.key, broker.asset(500)),
|
||||
ter(tecNO_PERMISSION));
|
||||
|
||||
{
|
||||
auto const otherAsset =
|
||||
broker.asset.raw() == assets[0].raw() ? assets[1]
|
||||
: assets[0];
|
||||
env(pay(borrower, loanKeylet.key, otherAsset(100)),
|
||||
ter(tecWRONG_ASSET));
|
||||
}
|
||||
|
||||
// Amount doesn't cover a single payment
|
||||
env(pay(borrower,
|
||||
loanKeylet.key,
|
||||
STAmount{broker.asset, 1}),
|
||||
ter(tecINSUFFICIENT_PAYMENT));
|
||||
|
||||
// Get the balance after these failed transactions take fees
|
||||
auto const borrowerBalanceBeforePayment =
|
||||
env.balance(borrower, broker.asset);
|
||||
|
||||
// Full payoff amount will consist of
|
||||
// Periodic payment amount will consist of
|
||||
// 1. principal outstanding (1000)
|
||||
// 2. accrued interest (at 12%)
|
||||
// 3. prepayment penalty (closeInterest at 3.6%)
|
||||
// 4. close payment fee (4)
|
||||
// Calculate these values without the helper functions to
|
||||
// verify they're working correctly
|
||||
// The numbers in the below BEAST_EXPECTs may not hold
|
||||
// across assets.
|
||||
// 2. interest interest rate (at 12%)
|
||||
// 3. payment interval (600s)
|
||||
// 4. loan service fee (2)
|
||||
// Calculate these values without the helper functions
|
||||
// to verify they're working correctly The numbers in
|
||||
// the below BEAST_EXPECTs may not hold across assets.
|
||||
Number const interval = state.paymentInterval;
|
||||
auto const periodicRate =
|
||||
interval * Number(12, -2) / (365 * 24 * 60 * 60);
|
||||
BEAST_EXPECT(
|
||||
periodicRate ==
|
||||
Number(2283105022831050, -21, Number::unchecked{}));
|
||||
STAmount const accruedInterest{
|
||||
broker.asset,
|
||||
state.principalOutstanding * periodicRate * loanAge /
|
||||
interval};
|
||||
BEAST_EXPECT(
|
||||
accruedInterest ==
|
||||
broker.asset(Number(1141552511415525, -19)));
|
||||
STAmount const prepaymentPenalty{
|
||||
broker.asset,
|
||||
state.principalOutstanding * Number(36, -3)};
|
||||
BEAST_EXPECT(prepaymentPenalty == broker.asset(36));
|
||||
STAmount const closePaymentFee = broker.asset(4);
|
||||
auto const payoffAmount =
|
||||
STAmount{broker.asset, state.principalOutstanding} +
|
||||
accruedInterest + prepaymentPenalty + closePaymentFee;
|
||||
BEAST_EXPECT(
|
||||
payoffAmount ==
|
||||
broker.asset(Number(1040000114155251, -12)));
|
||||
BEAST_EXPECT(payoffAmount > drawAmount);
|
||||
// Try to pay a little extra to show that it's _not_ taken
|
||||
auto const transactionAmount =
|
||||
payoffAmount + broker.asset(10);
|
||||
BEAST_EXPECT(
|
||||
transactionAmount ==
|
||||
broker.asset(Number(1050000114155251, -12)));
|
||||
env(pay(borrower, loanKeylet.key, transactionAmount));
|
||||
|
||||
env.close();
|
||||
|
||||
// Need to account for fees if the loan is in XRP
|
||||
adjustment = broker.asset(0);
|
||||
if (broker.asset.raw().native())
|
||||
while (state.paymentRemaining > 0)
|
||||
{
|
||||
adjustment = env.current()->fees().base;
|
||||
testcase << "Payments remaining: "
|
||||
<< state.paymentRemaining;
|
||||
// Compute the payment based on the number of payments
|
||||
// remaining
|
||||
auto const rateFactor =
|
||||
power(1 + periodicRate, state.paymentRemaining);
|
||||
STAmount const periodicPayment{
|
||||
broker.asset,
|
||||
state.principalOutstanding * periodicRate *
|
||||
rateFactor / (rateFactor - 1)};
|
||||
// Only check the first payment since the rounding may
|
||||
// drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
periodicPayment ==
|
||||
broker.asset(Number(8333457001162141, -14)));
|
||||
// Include the service fee
|
||||
STAmount const totalDue{
|
||||
broker.asset,
|
||||
periodicPayment + broker.asset(2).value()};
|
||||
// Only check the first payment since the rounding may
|
||||
// drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
totalDue ==
|
||||
broker.asset(Number(8533457001162141, -14)));
|
||||
|
||||
// Try to pay a little extra to show that it's _not_
|
||||
// taken
|
||||
STAmount const transactionAmount =
|
||||
STAmount{broker.asset, totalDue} + broker.asset(10);
|
||||
// Only check the first payment since the rounding may
|
||||
// drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
transactionAmount ==
|
||||
broker.asset(Number(9533457001162141, -14)));
|
||||
|
||||
auto const totalDueAmount =
|
||||
STAmount{broker.asset, totalDue};
|
||||
|
||||
// Compute the expected principal amount
|
||||
STAmount const interest{
|
||||
broker.asset,
|
||||
state.principalOutstanding * periodicRate};
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
interest ==
|
||||
broker.asset(Number(2283105022831050, -18)));
|
||||
BEAST_EXPECT(interest >= 0);
|
||||
|
||||
auto const principal = roundToAsset(
|
||||
broker.asset, periodicPayment - interest);
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
principal ==
|
||||
broker.asset(Number(8333228700000000, -14)));
|
||||
BEAST_EXPECT(
|
||||
principal > 0 &&
|
||||
principal <= state.principalOutstanding);
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining > 1 ||
|
||||
principal == state.principalOutstanding);
|
||||
|
||||
auto const borrowerBalanceBeforePayment =
|
||||
env.balance(borrower, broker.asset);
|
||||
|
||||
// Make the payment
|
||||
env(pay(borrower, loanKeylet.key, transactionAmount));
|
||||
|
||||
env.close();
|
||||
|
||||
// Need to account for fees if the loan is in XRP
|
||||
adjustment = broker.asset(0);
|
||||
if (broker.asset.raw().native())
|
||||
{
|
||||
adjustment = env.current()->fees().base;
|
||||
}
|
||||
|
||||
// Check the result
|
||||
BEAST_EXPECT(
|
||||
env.balance(borrower, broker.asset) ==
|
||||
borrowerBalanceBeforePayment - totalDueAmount -
|
||||
adjustment);
|
||||
|
||||
--state.paymentRemaining;
|
||||
state.previousPaymentDate = state.nextPaymentDate;
|
||||
state.nextPaymentDate += state.paymentInterval;
|
||||
state.principalOutstanding -= principal;
|
||||
|
||||
verifyLoanStatus(state);
|
||||
}
|
||||
|
||||
state.paymentRemaining = 0;
|
||||
state.principalOutstanding = 0;
|
||||
verifyLoanStatus(state);
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(borrower, broker.asset) ==
|
||||
borrowerBalanceBeforePayment - payoffAmount -
|
||||
adjustment);
|
||||
// Loan is paid off
|
||||
BEAST_EXPECT(state.paymentRemaining == 0);
|
||||
BEAST_EXPECT(state.principalOutstanding == 0);
|
||||
|
||||
// Can't impair or default a paid off loan
|
||||
env(manage(lender, loanKeylet.key, tfLoanImpair),
|
||||
@@ -1362,28 +1582,6 @@ class Loan_test : public beast::unit_test::suite
|
||||
ter(tecNO_PERMISSION));
|
||||
});
|
||||
|
||||
#if 0
|
||||
lifecycle(
|
||||
"Loan overpayment prohibited - Pay off",
|
||||
env,
|
||||
lender,
|
||||
borrower,
|
||||
evan,
|
||||
broker,
|
||||
pseudoAcct,
|
||||
0,
|
||||
[&](Keylet const& loanKeylet,
|
||||
VerifyLoanStatus const& verifyLoanStatus) {
|
||||
// toEndOfLife
|
||||
//
|
||||
// TODO: Draw and make some payments
|
||||
|
||||
// Make payments down to 0
|
||||
|
||||
// TODO: Try to impair a paid off loan
|
||||
});
|
||||
#endif
|
||||
|
||||
if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
|
||||
BEAST_EXPECT(brokerSle))
|
||||
{
|
||||
|
||||
@@ -33,6 +33,8 @@
|
||||
#include <xrpl/protocol/TER.h>
|
||||
#include <xrpl/protocol/st.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace ripple {
|
||||
|
||||
struct PreflightContext;
|
||||
@@ -69,29 +71,62 @@ loanPeriodicPayment(
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining);
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanTotalValueOutstanding(
|
||||
A asset,
|
||||
Number periodicPayment,
|
||||
std::uint32_t paymentsRemaining);
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return roundToAsset(
|
||||
asset, periodicPayment * paymentsRemaining, Number::upward);
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanTotalValueOutstanding(
|
||||
A asset,
|
||||
Number principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining);
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return loanTotalValueOutstanding(
|
||||
asset,
|
||||
loanPeriodicPayment(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
paymentsRemaining);
|
||||
}
|
||||
|
||||
Number
|
||||
inline Number
|
||||
loanTotalInterestOutstanding(
|
||||
Number principalOutstanding,
|
||||
Number totalValueOutstanding);
|
||||
Number totalValueOutstanding)
|
||||
{
|
||||
return totalValueOutstanding - principalOutstanding;
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanTotalInterestOutstanding(
|
||||
A asset,
|
||||
Number principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining);
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return loanTotalInterestOutstanding(
|
||||
principalOutstanding,
|
||||
loanTotalValueOutstanding(
|
||||
asset,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining));
|
||||
}
|
||||
|
||||
Number
|
||||
loanLatePaymentInterest(
|
||||
@@ -147,6 +182,17 @@ minusManagementFee(Number value, TenthBips32 managementFeeRate)
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
valueMinusManagementFee(
|
||||
A const& asset,
|
||||
Number value,
|
||||
TenthBips32 managementFeeRate)
|
||||
{
|
||||
return roundToAsset(
|
||||
asset, detail::minusManagementFee(value, managementFeeRate));
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanInterestOutstandingMinusFee(
|
||||
@@ -157,15 +203,15 @@ loanInterestOutstandingMinusFee(
|
||||
std::uint32_t paymentsRemaining,
|
||||
TenthBips32 managementFeeRate)
|
||||
{
|
||||
return roundToAsset(
|
||||
return valueMinusManagementFee(
|
||||
asset,
|
||||
detail::minusManagementFee(
|
||||
detail::loanTotalInterestOutstanding(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
managementFeeRate));
|
||||
detail::loanTotalInterestOutstanding(
|
||||
asset,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
managementFeeRate);
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
@@ -270,19 +316,23 @@ loanComputePaymentParts(
|
||||
auto const periodic = detail::computePeriodicPaymentParts(
|
||||
asset, principalOutstandingField, periodicPaymentAmount, periodicRate);
|
||||
|
||||
Number const totalValueOutstanding = roundToAsset(
|
||||
asset,
|
||||
detail::loanTotalValueOutstanding(
|
||||
periodicPaymentAmount, paymentRemainingField));
|
||||
Number const totalValueOutstanding = detail::loanTotalValueOutstanding(
|
||||
asset, periodicPaymentAmount, paymentRemainingField);
|
||||
XRPL_ASSERT(
|
||||
totalValueOutstanding > 0,
|
||||
"ripple::loanComputePaymentParts : valid total value");
|
||||
Number const totalInterestOutstanding =
|
||||
detail::loanTotalInterestOutstanding(
|
||||
principalOutstandingField, totalValueOutstanding);
|
||||
XRPL_ASSERT(
|
||||
XRPL_ASSERT_PARTS(
|
||||
totalInterestOutstanding >= 0,
|
||||
"ripple::loanComputePaymentParts : valid total interest");
|
||||
"ripple::loanComputePaymentParts",
|
||||
"valid total interest");
|
||||
XRPL_ASSERT_PARTS(
|
||||
totalValueOutstanding - totalInterestOutstanding ==
|
||||
principalOutstandingField,
|
||||
"ripple::loanComputePaymentParts",
|
||||
"valid principal computation");
|
||||
|
||||
view.update(loan);
|
||||
|
||||
@@ -388,8 +438,13 @@ loanComputePaymentParts(
|
||||
// periodic one, with possible overpayments
|
||||
|
||||
std::optional<NumberRoundModeGuard> mg(Number::downward);
|
||||
std::int64_t const fullPeriodicPayments{
|
||||
amount / roundToAsset(asset, periodicPaymentAmount, Number::upward)};
|
||||
std::int64_t const fullPeriodicPayments = [&]() {
|
||||
std::int64_t const full{
|
||||
amount /
|
||||
roundToAsset(
|
||||
asset, (periodicPaymentAmount + serviceFee), Number::upward)};
|
||||
return full < paymentRemainingField ? full : paymentRemainingField;
|
||||
}();
|
||||
mg.reset();
|
||||
// Temporary asserts
|
||||
XRPL_ASSERT(
|
||||
@@ -424,10 +479,10 @@ loanComputePaymentParts(
|
||||
periodicPaymentAmount,
|
||||
periodicRate);
|
||||
XRPL_ASSERT(
|
||||
future->interest < periodic.interest,
|
||||
future->interest <= periodic.interest,
|
||||
"ripple::loanComputePaymentParts : decreasing interest");
|
||||
XRPL_ASSERT(
|
||||
future->principal > periodic.principal,
|
||||
future->principal >= periodic.principal,
|
||||
"ripple::loanComputePaymentParts : increasing principal");
|
||||
|
||||
totalPrincipalPaid += future->principal;
|
||||
@@ -465,13 +520,12 @@ loanComputePaymentParts(
|
||||
|
||||
principalOutstandingField -= remainder;
|
||||
|
||||
Number const newInterest = roundToAsset(
|
||||
Number const newInterest = detail::loanTotalInterestOutstanding(
|
||||
asset,
|
||||
detail::loanTotalInterestOutstanding(
|
||||
principalOutstandingField,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentRemainingField));
|
||||
principalOutstandingField,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentRemainingField);
|
||||
|
||||
loanValueChange =
|
||||
(newInterest - totalInterestOutstanding) + interestPortion;
|
||||
|
||||
@@ -70,54 +70,6 @@ loanPeriodicPayment(
|
||||
principalOutstanding, periodicRate, paymentsRemaining);
|
||||
}
|
||||
|
||||
Number
|
||||
loanTotalValueOutstanding(
|
||||
Number periodicPayment,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return periodicPayment * paymentsRemaining;
|
||||
}
|
||||
|
||||
Number
|
||||
loanTotalValueOutstanding(
|
||||
Number principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return loanTotalValueOutstanding(
|
||||
loanPeriodicPayment(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
paymentsRemaining);
|
||||
}
|
||||
|
||||
Number
|
||||
loanTotalInterestOutstanding(
|
||||
Number principalOutstanding,
|
||||
Number totalValueOutstanding)
|
||||
{
|
||||
return totalValueOutstanding - principalOutstanding;
|
||||
}
|
||||
|
||||
Number
|
||||
loanTotalInterestOutstanding(
|
||||
Number principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return loanTotalInterestOutstanding(
|
||||
principalOutstanding,
|
||||
loanTotalValueOutstanding(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining));
|
||||
}
|
||||
|
||||
Number
|
||||
loanLatePaymentInterest(
|
||||
Number principalOutstanding,
|
||||
|
||||
@@ -212,7 +212,7 @@ LoanPay::doApply()
|
||||
auto const totalPaidToVault = paymentParts->principalPaid +
|
||||
paymentParts->interestPaid - managementFee;
|
||||
|
||||
auto const totalFee = paymentParts->feePaid + managementFee;
|
||||
auto const totalPaidToBroker = paymentParts->feePaid + managementFee;
|
||||
|
||||
// If there is not enough first-loss capital
|
||||
auto coverAvailableField = brokerSle->at(sfCoverAvailable);
|
||||
@@ -226,15 +226,13 @@ LoanPay::doApply()
|
||||
if (!sufficientCover)
|
||||
{
|
||||
// Add the fee to to First Loss Cover Pool
|
||||
coverAvailableField += totalFee;
|
||||
coverAvailableField += totalPaidToBroker;
|
||||
}
|
||||
|
||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
|
||||
// and subtract the change in the management fee
|
||||
auto const vaultValueChange = paymentParts->valueChange -
|
||||
roundToAsset(asset,
|
||||
tenthBipsOfValue(
|
||||
paymentParts->valueChange, managementFeeRate));
|
||||
auto const vaultValueChange = valueMinusManagementFee(
|
||||
asset, paymentParts->valueChange, managementFeeRate);
|
||||
debtTotalField += vaultValueChange - totalPaidToVault;
|
||||
|
||||
//------------------------------------------------------
|
||||
@@ -246,7 +244,7 @@ LoanPay::doApply()
|
||||
|
||||
// Move funds
|
||||
STAmount const paidToVault(asset, totalPaidToVault);
|
||||
STAmount const paidToBroker(asset, totalFee);
|
||||
STAmount const paidToBroker(asset, totalPaidToBroker);
|
||||
XRPL_ASSERT_PARTS(
|
||||
paidToVault + paidToBroker <= amount,
|
||||
"ripple::LoanPay::doApply",
|
||||
|
||||
@@ -246,7 +246,7 @@ LoanSet::preclaim(PreclaimContext const& ctx)
|
||||
if (auto const originationFee = tx[~sfLoanOriginationFee])
|
||||
{
|
||||
// Check that the lender will not make an unfair profit on the lending
|
||||
// fee if the loan defaults. (Not yet in spec. May not be included.)
|
||||
// fee if the loan defaults. (Not yet in spec. May need to be removed.)
|
||||
TenthBips32 const coverRateLiquidation{
|
||||
brokerSle->at(sfCoverRateLiquidation)};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user