mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-22 12:05:53 +00:00
Expand LoanPay test cases, and fix rounding errors
- At least rounding errors for a full payoff
This commit is contained in:
@@ -697,12 +697,14 @@ getRate(STAmount const& offerOut, STAmount const& offerIn);
|
|||||||
|
|
||||||
template <AssetType A>
|
template <AssetType A>
|
||||||
Number
|
Number
|
||||||
roundToAsset(A const& asset, Number const& value)
|
roundToAsset(
|
||||||
|
A const& asset,
|
||||||
|
Number const& value,
|
||||||
|
Number::rounding_mode rounding = Number::getround())
|
||||||
{
|
{
|
||||||
|
NumberRoundModeGuard mg(rounding);
|
||||||
STAmount const amount{asset, value};
|
STAmount const amount{asset, value};
|
||||||
if (amount != value)
|
|
||||||
return amount;
|
return amount;
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1229,7 +1229,7 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
// toEndOfLife
|
// toEndOfLife
|
||||||
//
|
//
|
||||||
auto state = currentState(loanKeylet, verifyLoanStatus);
|
auto state = currentState(loanKeylet, verifyLoanStatus);
|
||||||
BEAST_EXPECT(state.flags == lsfLoanOverpayment);
|
BEAST_EXPECT(state.flags == 0);
|
||||||
auto const borrowerStartingBalance =
|
auto const borrowerStartingBalance =
|
||||||
env.balance(borrower, broker.asset);
|
env.balance(borrower, broker.asset);
|
||||||
|
|
||||||
@@ -1246,7 +1246,7 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
PrettyAmount adjustment = broker.asset(0);
|
PrettyAmount adjustment = broker.asset(0);
|
||||||
if (broker.asset.raw().native())
|
if (broker.asset.raw().native())
|
||||||
{
|
{
|
||||||
adjustment = env.current()->fees().base;
|
adjustment = 2 * env.current()->fees().base;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw the entire available balance
|
// Draw the entire available balance
|
||||||
@@ -1256,6 +1256,8 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
broker.asset, state.assetsAvailable};
|
broker.asset, state.assetsAvailable};
|
||||||
env(draw(borrower, loanKeylet.key, drawAmount));
|
env(draw(borrower, loanKeylet.key, drawAmount));
|
||||||
env.close(state.startDate + 20s);
|
env.close(state.startDate + 20s);
|
||||||
|
auto const loanAge = (env.now() - state.startDate).count();
|
||||||
|
BEAST_EXPECT(loanAge == 30);
|
||||||
|
|
||||||
state.assetsAvailable -= drawAmount;
|
state.assetsAvailable -= drawAmount;
|
||||||
verifyLoanStatus(state);
|
verifyLoanStatus(state);
|
||||||
@@ -1283,8 +1285,14 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
ter(tecWRONG_ASSET));
|
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
|
// Get the balance after these failed transactions take fees
|
||||||
auto const borrowerBeforePaymentBalance =
|
auto const borrowerBalanceBeforePayment =
|
||||||
env.balance(borrower, broker.asset);
|
env.balance(borrower, broker.asset);
|
||||||
|
|
||||||
// Full payoff amount will consist of
|
// Full payoff amount will consist of
|
||||||
@@ -1294,27 +1302,43 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
// 4. close payment fee (4)
|
// 4. close payment fee (4)
|
||||||
// Calculate these values without the helper functions to
|
// Calculate these values without the helper functions to
|
||||||
// verify they're working correctly
|
// verify they're working correctly
|
||||||
|
// The numbers in the below BEAST_EXPECTs may not hold
|
||||||
|
// across assets.
|
||||||
Number const interval = state.paymentInterval;
|
Number const interval = state.paymentInterval;
|
||||||
auto const periodicRate =
|
auto const periodicRate =
|
||||||
interval * (12 / 100) / (365 * 24 * 60 * 60);
|
interval * Number(12, -2) / (365 * 24 * 60 * 60);
|
||||||
// 20 seconds since the loan started (last env.close call)
|
BEAST_EXPECT(
|
||||||
|
periodicRate ==
|
||||||
|
Number(2283105022831050, -21, Number::unchecked{}));
|
||||||
STAmount const accruedInterest{
|
STAmount const accruedInterest{
|
||||||
broker.asset,
|
broker.asset,
|
||||||
state.principalOutstanding * periodicRate * 20 /
|
state.principalOutstanding * periodicRate * loanAge /
|
||||||
interval};
|
interval};
|
||||||
|
BEAST_EXPECT(
|
||||||
|
accruedInterest ==
|
||||||
|
broker.asset(Number(1141552511415525, -19)));
|
||||||
STAmount const prepaymentPenalty{
|
STAmount const prepaymentPenalty{
|
||||||
broker.asset,
|
broker.asset,
|
||||||
state.principalOutstanding * 36 / 10 / 100};
|
state.principalOutstanding * Number(36, -3)};
|
||||||
|
BEAST_EXPECT(prepaymentPenalty == broker.asset(36));
|
||||||
STAmount const closePaymentFee = broker.asset(4);
|
STAmount const closePaymentFee = broker.asset(4);
|
||||||
auto const payoffAmount =
|
auto const payoffAmount =
|
||||||
STAmount{broker.asset, state.principalOutstanding} +
|
STAmount{broker.asset, state.principalOutstanding} +
|
||||||
accruedInterest + prepaymentPenalty + closePaymentFee;
|
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
|
// Try to pay a little extra to show that it's _not_ taken
|
||||||
auto const transactionAmount =
|
auto const transactionAmount =
|
||||||
payoffAmount + broker.asset(10);
|
payoffAmount + broker.asset(10);
|
||||||
BEAST_EXPECT(payoffAmount > drawAmount);
|
BEAST_EXPECT(
|
||||||
|
transactionAmount ==
|
||||||
|
broker.asset(Number(1050000114155251, -12)));
|
||||||
env(pay(borrower, loanKeylet.key, transactionAmount));
|
env(pay(borrower, loanKeylet.key, transactionAmount));
|
||||||
|
|
||||||
|
env.close();
|
||||||
|
|
||||||
// Need to account for fees if the loan is in XRP
|
// Need to account for fees if the loan is in XRP
|
||||||
adjustment = broker.asset(0);
|
adjustment = broker.asset(0);
|
||||||
if (broker.asset.raw().native())
|
if (broker.asset.raw().native())
|
||||||
@@ -1328,10 +1352,14 @@ class Loan_test : public beast::unit_test::suite
|
|||||||
|
|
||||||
BEAST_EXPECT(
|
BEAST_EXPECT(
|
||||||
env.balance(borrower, broker.asset) ==
|
env.balance(borrower, broker.asset) ==
|
||||||
borrowerBeforePaymentBalance - payoffAmount -
|
borrowerBalanceBeforePayment - payoffAmount -
|
||||||
adjustment);
|
adjustment);
|
||||||
|
|
||||||
// TODO: Try to impair a paid off loan
|
// 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));
|
||||||
});
|
});
|
||||||
|
|
||||||
#if 0
|
#if 0
|
||||||
|
|||||||
@@ -216,13 +216,13 @@ public:
|
|||||||
PrettyAmount
|
PrettyAmount
|
||||||
operator()(T v) const
|
operator()(T v) const
|
||||||
{
|
{
|
||||||
bool negative = false;
|
return operator()(Number(v));
|
||||||
if (v < 0)
|
|
||||||
{
|
|
||||||
v = -v;
|
|
||||||
negative = true;
|
|
||||||
}
|
}
|
||||||
STAmount amount{asset_, v * scale_, 0, negative};
|
|
||||||
|
PrettyAmount
|
||||||
|
operator()(Number v) const
|
||||||
|
{
|
||||||
|
STAmount amount{asset_, v * scale_};
|
||||||
return {amount, ""};
|
return {amount, ""};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,10 +127,11 @@ computePeriodicPaymentParts(
|
|||||||
Number const interest =
|
Number const interest =
|
||||||
roundToAsset(asset, principalOutstanding * periodicRate);
|
roundToAsset(asset, principalOutstanding * periodicRate);
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
interest > 0,
|
interest >= 0,
|
||||||
"ripple::detail::computePeriodicPayment : valid interest");
|
"ripple::detail::computePeriodicPayment : valid interest");
|
||||||
|
|
||||||
Number const principal = periodicPaymentAmount - interest;
|
Number const principal =
|
||||||
|
roundToAsset(asset, periodicPaymentAmount - interest);
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
principal > 0 && principal <= principalOutstanding,
|
principal > 0 && principal <= principalOutstanding,
|
||||||
"ripple::detail::computePeriodicPayment : valid principal");
|
"ripple::detail::computePeriodicPayment : valid principal");
|
||||||
@@ -233,7 +234,8 @@ loanComputePaymentParts(
|
|||||||
|
|
||||||
Number const serviceFee = loan->at(sfLoanServiceFee);
|
Number const serviceFee = loan->at(sfLoanServiceFee);
|
||||||
Number const latePaymentFee = loan->at(sfLatePaymentFee);
|
Number const latePaymentFee = loan->at(sfLatePaymentFee);
|
||||||
Number const closePaymentFee = loan->at(sfClosePaymentFee);
|
Number const closePaymentFee =
|
||||||
|
roundToAsset(asset, loan->at(sfClosePaymentFee));
|
||||||
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
|
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
|
||||||
|
|
||||||
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
|
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
|
||||||
@@ -257,10 +259,10 @@ loanComputePaymentParts(
|
|||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
periodicRate > 0, "ripple::loanComputePaymentParts : valid rate");
|
periodicRate > 0, "ripple::loanComputePaymentParts : valid rate");
|
||||||
|
|
||||||
Number const periodicPaymentAmount = roundToAsset(
|
// Don't round the payment amount. Only round the final computations using
|
||||||
asset,
|
// it.
|
||||||
detail::loanPeriodicPayment(
|
Number const periodicPaymentAmount = detail::loanPeriodicPayment(
|
||||||
principalOutstandingField, periodicRate, paymentRemainingField));
|
principalOutstandingField, periodicRate, paymentRemainingField);
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
periodicPaymentAmount > 0,
|
periodicPaymentAmount > 0,
|
||||||
"ripple::computePeriodicPayment : valid payment");
|
"ripple::computePeriodicPayment : valid payment");
|
||||||
@@ -268,8 +270,10 @@ loanComputePaymentParts(
|
|||||||
auto const periodic = detail::computePeriodicPaymentParts(
|
auto const periodic = detail::computePeriodicPaymentParts(
|
||||||
asset, principalOutstandingField, periodicPaymentAmount, periodicRate);
|
asset, principalOutstandingField, periodicPaymentAmount, periodicRate);
|
||||||
|
|
||||||
Number const totalValueOutstanding = detail::loanTotalValueOutstanding(
|
Number const totalValueOutstanding = roundToAsset(
|
||||||
periodicPaymentAmount, paymentRemainingField);
|
asset,
|
||||||
|
detail::loanTotalValueOutstanding(
|
||||||
|
periodicPaymentAmount, paymentRemainingField));
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
totalValueOutstanding > 0,
|
totalValueOutstanding > 0,
|
||||||
"ripple::loanComputePaymentParts : valid total value");
|
"ripple::loanComputePaymentParts : valid total value");
|
||||||
@@ -277,7 +281,7 @@ loanComputePaymentParts(
|
|||||||
detail::loanTotalInterestOutstanding(
|
detail::loanTotalInterestOutstanding(
|
||||||
principalOutstandingField, totalValueOutstanding);
|
principalOutstandingField, totalValueOutstanding);
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
totalInterestOutstanding > 0,
|
totalInterestOutstanding >= 0,
|
||||||
"ripple::loanComputePaymentParts : valid total interest");
|
"ripple::loanComputePaymentParts : valid total interest");
|
||||||
|
|
||||||
view.update(loan);
|
view.update(loan);
|
||||||
@@ -351,29 +355,29 @@ loanComputePaymentParts(
|
|||||||
closePrepaymentInterest >= 0,
|
closePrepaymentInterest >= 0,
|
||||||
"ripple::loanComputePaymentParts : valid prepayment "
|
"ripple::loanComputePaymentParts : valid prepayment "
|
||||||
"interest");
|
"interest");
|
||||||
auto const closeFullPayment = principalOutstandingField +
|
auto const totalInterest = accruedInterest + closePrepaymentInterest;
|
||||||
accruedInterest + closePrepaymentInterest + closePaymentFee;
|
auto const closeFullPayment =
|
||||||
|
principalOutstandingField + totalInterest + closePaymentFee;
|
||||||
|
|
||||||
// if the payment is equal or higher than full payment amount, make a
|
// if the payment is equal or higher than full payment amount, make a
|
||||||
// full payment
|
// full payment
|
||||||
if (amount >= closeFullPayment)
|
if (amount >= closeFullPayment)
|
||||||
{
|
{
|
||||||
paymentRemainingField = 0;
|
|
||||||
principalOutstandingField = 0;
|
|
||||||
|
|
||||||
// A full payment decreases the value of the loan by the
|
// A full payment decreases the value of the loan by the
|
||||||
// difference between the interest paid and the expected
|
// difference between the interest paid and the expected
|
||||||
// outstanding interest return
|
// outstanding interest return
|
||||||
auto const valueChange = accruedInterest - totalInterestOutstanding;
|
auto const valueChange = totalInterest - totalInterestOutstanding;
|
||||||
XRPL_ASSERT(
|
|
||||||
valueChange <= 0,
|
LoanPaymentParts const result{
|
||||||
"ripple::loanComputePaymentParts : valid full payment "
|
|
||||||
"value change");
|
|
||||||
return LoanPaymentParts{
|
|
||||||
principalOutstandingField,
|
principalOutstandingField,
|
||||||
accruedInterest,
|
totalInterest,
|
||||||
valueChange,
|
valueChange,
|
||||||
closePaymentFee};
|
closePaymentFee};
|
||||||
|
|
||||||
|
paymentRemainingField = 0;
|
||||||
|
principalOutstandingField = 0;
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,7 +388,8 @@ loanComputePaymentParts(
|
|||||||
// periodic one, with possible overpayments
|
// periodic one, with possible overpayments
|
||||||
|
|
||||||
std::optional<NumberRoundModeGuard> mg(Number::downward);
|
std::optional<NumberRoundModeGuard> mg(Number::downward);
|
||||||
std::int64_t const fullPeriodicPayments{amount / periodicPaymentAmount};
|
std::int64_t const fullPeriodicPayments{
|
||||||
|
amount / roundToAsset(asset, periodicPaymentAmount, Number::upward)};
|
||||||
mg.reset();
|
mg.reset();
|
||||||
// Temporary asserts
|
// Temporary asserts
|
||||||
XRPL_ASSERT(
|
XRPL_ASSERT(
|
||||||
@@ -441,13 +446,14 @@ loanComputePaymentParts(
|
|||||||
principalOutstandingField.value(),
|
principalOutstandingField.value(),
|
||||||
amount - periodicPaymentAmount * fullPeriodicPayments);
|
amount - periodicPaymentAmount * fullPeriodicPayments);
|
||||||
|
|
||||||
if (overpayment > 0)
|
if (roundToAsset(asset, overpayment) > 0)
|
||||||
{
|
{
|
||||||
Number const interestPortion = roundToAsset(
|
Number const interestPortion = roundToAsset(
|
||||||
asset, tenthBipsOfValue(overpayment, overpaymentInterestRate));
|
asset, tenthBipsOfValue(overpayment, overpaymentInterestRate));
|
||||||
Number const feePortion = roundToAsset(
|
Number const feePortion = roundToAsset(
|
||||||
asset, tenthBipsOfValue(overpayment, overpaymentFee));
|
asset, tenthBipsOfValue(overpayment, overpaymentFee));
|
||||||
Number const remainder = overpayment - interestPortion - feePortion;
|
Number const remainder =
|
||||||
|
roundToAsset(asset, overpayment - interestPortion - feePortion);
|
||||||
|
|
||||||
// Don't process an overpayment if the whole amount (or more!) gets
|
// Don't process an overpayment if the whole amount (or more!) gets
|
||||||
// eaten by fees
|
// eaten by fees
|
||||||
|
|||||||
@@ -188,6 +188,19 @@ LoanPay::doApply()
|
|||||||
// If the loan was impaired, it isn't anymore.
|
// If the loan was impaired, it isn't anymore.
|
||||||
loanSle->clearFlag(lsfLoanImpaired);
|
loanSle->clearFlag(lsfLoanImpaired);
|
||||||
|
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
paymentParts->principalPaid > 0,
|
||||||
|
"ripple::LoanPay::doApply",
|
||||||
|
"valid principal paid");
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
paymentParts->interestPaid >= 0,
|
||||||
|
"ripple::LoanPay::doApply",
|
||||||
|
"valid interest paid");
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
paymentParts->feePaid >= 0,
|
||||||
|
"ripple::LoanPay::doApply",
|
||||||
|
"valid fee paid");
|
||||||
|
|
||||||
//------------------------------------------------------
|
//------------------------------------------------------
|
||||||
// LoanBroker object state changes
|
// LoanBroker object state changes
|
||||||
view.update(brokerSle);
|
view.update(brokerSle);
|
||||||
@@ -206,8 +219,10 @@ LoanPay::doApply()
|
|||||||
auto debtTotalField = brokerSle->at(sfDebtTotal);
|
auto debtTotalField = brokerSle->at(sfDebtTotal);
|
||||||
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)};
|
||||||
|
|
||||||
bool const sufficientCover = coverAvailableField >=
|
bool const sufficientCover =
|
||||||
tenthBipsOfValue(debtTotalField.value(), coverRateMinimum);
|
coverAvailableField >=
|
||||||
|
roundToAsset(
|
||||||
|
asset, tenthBipsOfValue(debtTotalField.value(), coverRateMinimum));
|
||||||
if (!sufficientCover)
|
if (!sufficientCover)
|
||||||
{
|
{
|
||||||
// Add the fee to to First Loss Cover Pool
|
// Add the fee to to First Loss Cover Pool
|
||||||
@@ -217,7 +232,9 @@ LoanPay::doApply()
|
|||||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
|
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
|
||||||
// and subtract the change in the management fee
|
// and subtract the change in the management fee
|
||||||
auto const vaultValueChange = paymentParts->valueChange -
|
auto const vaultValueChange = paymentParts->valueChange -
|
||||||
tenthBipsOfValue(paymentParts->valueChange, managementFeeRate);
|
roundToAsset(asset,
|
||||||
|
tenthBipsOfValue(
|
||||||
|
paymentParts->valueChange, managementFeeRate));
|
||||||
debtTotalField += vaultValueChange - totalPaidToVault;
|
debtTotalField += vaultValueChange - totalPaidToVault;
|
||||||
|
|
||||||
//------------------------------------------------------
|
//------------------------------------------------------
|
||||||
@@ -231,9 +248,14 @@ LoanPay::doApply()
|
|||||||
STAmount const paidToVault(asset, totalPaidToVault);
|
STAmount const paidToVault(asset, totalPaidToVault);
|
||||||
STAmount const paidToBroker(asset, totalFee);
|
STAmount const paidToBroker(asset, totalFee);
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
paidToVault + paidToBroker == amount,
|
paidToVault + paidToBroker <= amount,
|
||||||
"ripple::LoanPay::doApply",
|
"ripple::LoanPay::doApply",
|
||||||
"correct payment totals");
|
"amount is sufficient");
|
||||||
|
XRPL_ASSERT_PARTS(
|
||||||
|
paidToVault + paidToBroker <= paymentParts->principalPaid +
|
||||||
|
paymentParts->interestPaid + paymentParts->feePaid,
|
||||||
|
"ripple::LoanPay::doApply",
|
||||||
|
"payment agreement");
|
||||||
|
|
||||||
if (auto const ter = accountSend(
|
if (auto const ter = accountSend(
|
||||||
view,
|
view,
|
||||||
|
|||||||
Reference in New Issue
Block a user