mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-28 06:55:50 +00:00
Fix or work around loan payment rounding problems
This commit is contained in:
@@ -695,26 +695,11 @@ divRoundStrict(
|
||||
std::uint64_t
|
||||
getRate(STAmount const& offerOut, STAmount const& offerIn);
|
||||
|
||||
inline STAmount
|
||||
STAmount
|
||||
roundToReference(
|
||||
STAmount const value,
|
||||
STAmount const referenceValue,
|
||||
Number::rounding_mode rounding = Number::getround())
|
||||
{
|
||||
NumberRoundModeGuard mg(rounding);
|
||||
if (value >= referenceValue)
|
||||
return value;
|
||||
// With an IOU, the total will be truncated to the precision of the
|
||||
// larger value: referenceValue
|
||||
STAmount const total = referenceValue + value;
|
||||
STAmount const result = total - referenceValue;
|
||||
XRPL_ASSERT_PARTS(
|
||||
(!value.asset().native() && value.asset().holds<Issue>()) ||
|
||||
value == result,
|
||||
"ripple::roundToReference",
|
||||
"rounding only on IOU");
|
||||
return result;
|
||||
}
|
||||
STAmount referenceValue,
|
||||
Number::rounding_mode rounding = Number::getround());
|
||||
|
||||
/** Round an arbitrary precision Number to the precision of a given Asset.
|
||||
*
|
||||
@@ -734,8 +719,10 @@ roundToAsset(
|
||||
Number::rounding_mode rounding = Number::getround())
|
||||
{
|
||||
NumberRoundModeGuard mg(rounding);
|
||||
return roundToReference(
|
||||
STAmount{asset, value}, STAmount{asset, referenceValue});
|
||||
STAmount const ret{asset, value};
|
||||
if (ret.asset().native() || !ret.asset().holds<Issue>())
|
||||
return ret;
|
||||
return roundToReference(ret, STAmount{asset, referenceValue});
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -553,6 +553,9 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
|
||||
{sfPaymentRemaining, soeREQUIRED},
|
||||
{sfAssetsAvailable, soeREQUIRED},
|
||||
{sfPrincipalOutstanding, soeREQUIRED},
|
||||
// Save the original request amount for rounding / scaling of
|
||||
// other computations, particularly for IOUs
|
||||
{sfPrincipalRequested, soeREQUIRED},
|
||||
}))
|
||||
|
||||
#undef EXPAND
|
||||
|
||||
@@ -1353,6 +1353,30 @@ canonicalizeRoundStrict(
|
||||
}
|
||||
}
|
||||
|
||||
STAmount
|
||||
roundToReference(
|
||||
STAmount const value,
|
||||
STAmount referenceValue,
|
||||
Number::rounding_mode rounding)
|
||||
{
|
||||
if (value.asset().native() || !value.asset().holds<Issue>())
|
||||
return value;
|
||||
|
||||
NumberRoundModeGuard mg(rounding);
|
||||
if (referenceValue.negative() != value.negative())
|
||||
referenceValue.negate();
|
||||
|
||||
if (value.exponent() > referenceValue.exponent() &&
|
||||
(value.exponent() == referenceValue.exponent() &&
|
||||
value.mantissa() >= referenceValue.mantissa()))
|
||||
return value;
|
||||
// With an IOU, the total will be truncated to the precision of the
|
||||
// larger value: referenceValue
|
||||
STAmount const total = referenceValue + value;
|
||||
STAmount const result = total - referenceValue;
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// We need a class that has an interface similar to NumberRoundModeGuard
|
||||
|
||||
@@ -117,6 +117,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
std::uint32_t nextPaymentDate = 0;
|
||||
std::uint32_t paymentRemaining = 0;
|
||||
Number assetsAvailable = 0;
|
||||
Number const principalRequested;
|
||||
Number principalOutstanding = 0;
|
||||
std::uint32_t flags = 0;
|
||||
std::uint32_t paymentInterval = 0;
|
||||
@@ -145,6 +146,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
void
|
||||
checkBroker(
|
||||
Number const& assetsAvailable,
|
||||
Number const& principalRequested,
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
@@ -159,6 +161,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
brokerSle->at(sfManagementFeeRate)};
|
||||
auto const loanInterest = loanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
principalRequested,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
@@ -171,7 +174,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
brokerDebt == expectedDebt ||
|
||||
(expectedDebt != Number(0) &&
|
||||
((brokerDebt - expectedDebt) / expectedDebt <
|
||||
Number(1, -2))));
|
||||
Number(1, -8))));
|
||||
env.test.BEAST_EXPECT(
|
||||
env.balance(pseudoAccount, broker.asset).number() ==
|
||||
brokerSle->at(sfCoverAvailable) + assetsAvailable);
|
||||
@@ -189,9 +192,16 @@ class Loan_test : public beast::unit_test::suite
|
||||
env.balance(vaultPseudo, broker.asset).number());
|
||||
if (ownerCount == 0)
|
||||
{
|
||||
// Allow some slop for rounding IOUs
|
||||
auto const total = vaultSle->at(sfAssetsTotal);
|
||||
auto const available = vaultSle->at(sfAssetsAvailable);
|
||||
env.test.BEAST_EXPECT(
|
||||
vaultSle->at(sfAssetsTotal) ==
|
||||
vaultSle->at(sfAssetsAvailable));
|
||||
total == available ||
|
||||
(!broker.asset.raw().native() &&
|
||||
broker.asset.raw().holds<Issue>() &&
|
||||
available != 0 &&
|
||||
((total - available) / available <
|
||||
Number(1, -6))));
|
||||
env.test.BEAST_EXPECT(
|
||||
vaultSle->at(sfLossUnrealized) == 0);
|
||||
}
|
||||
@@ -207,6 +217,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
{
|
||||
checkBroker(
|
||||
state.assetsAvailable,
|
||||
state.principalRequested,
|
||||
state.principalOutstanding,
|
||||
interestRate,
|
||||
state.paymentInterval,
|
||||
@@ -220,6 +231,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
std::uint32_t nextPaymentDate,
|
||||
std::uint32_t paymentRemaining,
|
||||
Number const& assetsAvailable,
|
||||
Number const& principalRequested,
|
||||
Number const& principalOutstanding,
|
||||
std::uint32_t flags) const
|
||||
{
|
||||
@@ -234,14 +246,20 @@ class Loan_test : public beast::unit_test::suite
|
||||
loan->at(sfPaymentRemaining) == paymentRemaining);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfAssetsAvailable) == assetsAvailable);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfPrincipalRequested) == principalRequested);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfPrincipalOutstanding) == principalOutstanding);
|
||||
env.test.BEAST_EXPECT(
|
||||
loan->at(sfPrincipalRequested) ==
|
||||
broker.asset(1000).value());
|
||||
env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
|
||||
|
||||
auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
|
||||
auto const paymentInterval = loan->at(sfPaymentInterval);
|
||||
checkBroker(
|
||||
assetsAvailable,
|
||||
principalRequested,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
@@ -266,6 +284,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
principalOutstanding +
|
||||
loanInterestOutstandingMinusFee(
|
||||
broker.asset,
|
||||
principalRequested,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
@@ -290,6 +309,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
state.nextPaymentDate,
|
||||
state.paymentRemaining,
|
||||
state.assetsAvailable,
|
||||
state.principalRequested,
|
||||
state.principalOutstanding,
|
||||
state.flags);
|
||||
};
|
||||
@@ -332,7 +352,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
env, broker, pseudoAcct, keylet);
|
||||
|
||||
// No loans yet
|
||||
verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0);
|
||||
verifyLoanStatus.checkBroker(
|
||||
0, broker.asset(1000).value(), 0, TenthBips32{0}, 1, 0, 0);
|
||||
|
||||
if (!BEAST_EXPECT(loanSequence != 0))
|
||||
return;
|
||||
@@ -433,6 +454,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
BEAST_EXPECT(
|
||||
loan->at(sfAssetsAvailable) ==
|
||||
principalRequest - originationFee);
|
||||
BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest);
|
||||
BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest);
|
||||
}
|
||||
|
||||
@@ -442,6 +464,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
total,
|
||||
principalRequest - originationFee,
|
||||
principalRequest,
|
||||
principalRequest,
|
||||
loanFlags | 0);
|
||||
|
||||
// Manage the loan
|
||||
@@ -487,6 +510,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
total,
|
||||
principalRequest - originationFee,
|
||||
principalRequest,
|
||||
principalRequest,
|
||||
loanFlags | 0);
|
||||
|
||||
// Can't delete the loan yet. It has payments remaining.
|
||||
@@ -525,7 +549,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
env.close();
|
||||
|
||||
// No loans left
|
||||
verifyLoanStatus.checkBroker(0, 0, interest, 1, 0, 0);
|
||||
verifyLoanStatus.checkBroker(
|
||||
0, broker.asset(1000).value(), 0, interest, 1, 0, 0);
|
||||
|
||||
BEAST_EXPECT(
|
||||
env.balance(borrower, broker.asset).value() ==
|
||||
@@ -592,7 +617,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
env(pay(issuer, borrower, mptAsset(1'000)));
|
||||
env.close();
|
||||
|
||||
std::array const assets{xrpAsset, iouAsset, mptAsset};
|
||||
std::array const assets{xrpAsset, mptAsset, iouAsset};
|
||||
|
||||
auto const coverDepositParameter = 1000;
|
||||
auto const coverRateMinParameter = percentageToTenthBips(10);
|
||||
@@ -1015,33 +1040,49 @@ class Loan_test : public beast::unit_test::suite
|
||||
auto currentState = [&](Keylet const& loanKeylet,
|
||||
VerifyLoanStatus const& verifyLoanStatus) {
|
||||
// Lookup the current loan state
|
||||
LoanState state;
|
||||
if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
|
||||
{
|
||||
state.previousPaymentDate = loan->at(sfPreviousPaymentDate);
|
||||
LoanState state{
|
||||
.previousPaymentDate = loan->at(sfPreviousPaymentDate),
|
||||
.startDate = tp{d{loan->at(sfStartDate)}},
|
||||
.nextPaymentDate = loan->at(sfNextPaymentDueDate),
|
||||
.paymentRemaining = loan->at(sfPaymentRemaining),
|
||||
.assetsAvailable = loan->at(sfAssetsAvailable),
|
||||
.principalRequested = loan->at(sfPrincipalRequested),
|
||||
.principalOutstanding =
|
||||
loan->at(sfPrincipalOutstanding),
|
||||
.flags = loan->at(sfFlags),
|
||||
.paymentInterval = loan->at(sfPaymentInterval),
|
||||
};
|
||||
BEAST_EXPECT(state.previousPaymentDate == 0);
|
||||
state.startDate = tp{d{loan->at(sfStartDate)}};
|
||||
state.nextPaymentDate = loan->at(sfNextPaymentDueDate);
|
||||
BEAST_EXPECT(
|
||||
tp{d{state.nextPaymentDate}} == state.startDate + 600s);
|
||||
state.paymentRemaining = loan->at(sfPaymentRemaining);
|
||||
BEAST_EXPECT(state.paymentRemaining == 12);
|
||||
state.assetsAvailable = loan->at(sfAssetsAvailable);
|
||||
BEAST_EXPECT(
|
||||
state.assetsAvailable == broker.asset(999).value());
|
||||
state.principalOutstanding =
|
||||
loan->at(sfPrincipalOutstanding);
|
||||
BEAST_EXPECT(
|
||||
state.principalOutstanding ==
|
||||
broker.asset(1000).value());
|
||||
state.paymentInterval = loan->at(sfPaymentInterval);
|
||||
BEAST_EXPECT(
|
||||
state.principalOutstanding == state.principalRequested);
|
||||
BEAST_EXPECT(state.paymentInterval == 600);
|
||||
state.flags = loan->at(sfFlags);
|
||||
|
||||
verifyLoanStatus(state);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
verifyLoanStatus(state);
|
||||
|
||||
return state;
|
||||
return LoanState{
|
||||
.previousPaymentDate = 0,
|
||||
.startDate = tp{d{0}},
|
||||
.nextPaymentDate = 0,
|
||||
.paymentRemaining = 0,
|
||||
.assetsAvailable = 0,
|
||||
.principalRequested = 0,
|
||||
.principalOutstanding = 0,
|
||||
.flags = 0,
|
||||
.paymentInterval = 0,
|
||||
};
|
||||
};
|
||||
|
||||
auto defaultBeforeStartDate = [&](std::uint32_t baseFlag,
|
||||
@@ -1486,8 +1527,8 @@ class Loan_test : public beast::unit_test::suite
|
||||
testcase << "Payments remaining: "
|
||||
<< state.paymentRemaining;
|
||||
|
||||
STAmount const principalOutstandingAmount{
|
||||
broker.asset, state.principalOutstanding};
|
||||
STAmount const principalRequestedAmount{
|
||||
broker.asset, state.principalRequested};
|
||||
// Compute the payment based on the number of payments
|
||||
// remaining
|
||||
auto const rateFactor =
|
||||
@@ -1497,7 +1538,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
rateFactor / (rateFactor - 1);
|
||||
STAmount const periodicPayment = roundToReference(
|
||||
STAmount{broker.asset, rawPeriodicPayment},
|
||||
principalOutstandingAmount);
|
||||
principalRequestedAmount);
|
||||
// Only check the first payment since the rounding may
|
||||
// drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
@@ -1507,7 +1548,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
// Include the service fee
|
||||
STAmount const totalDue = roundToReference(
|
||||
periodicPayment + broker.asset(2),
|
||||
principalOutstandingAmount);
|
||||
principalRequestedAmount);
|
||||
// Only check the first payment since the rounding may
|
||||
// drift as payments are made
|
||||
BEAST_EXPECT(
|
||||
@@ -1515,7 +1556,7 @@ class Loan_test : public beast::unit_test::suite
|
||||
totalDue ==
|
||||
roundToReference(
|
||||
broker.asset(Number(8533457001162141, -14)),
|
||||
principalOutstandingAmount));
|
||||
principalRequestedAmount));
|
||||
|
||||
// Try to pay a little extra to show that it's _not_
|
||||
// taken
|
||||
@@ -1528,25 +1569,26 @@ class Loan_test : public beast::unit_test::suite
|
||||
transactionAmount ==
|
||||
roundToReference(
|
||||
broker.asset(Number(9533457001162141, -14)),
|
||||
principalOutstandingAmount));
|
||||
principalRequestedAmount));
|
||||
|
||||
auto const totalDueAmount =
|
||||
STAmount{broker.asset, totalDue};
|
||||
|
||||
// Compute the expected principal amount
|
||||
Number const rawInterest =
|
||||
state.principalOutstanding * periodicRate;
|
||||
Number const rawInterest = state.paymentRemaining == 1
|
||||
? rawPeriodicPayment - state.principalOutstanding
|
||||
: state.principalOutstanding * periodicRate;
|
||||
STAmount const interest = roundToReference(
|
||||
STAmount{broker.asset, rawInterest},
|
||||
principalOutstandingAmount);
|
||||
principalRequestedAmount);
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining < 12 ||
|
||||
roundToReference(
|
||||
STAmount{broker.asset, rawInterest},
|
||||
principalOutstandingAmount) ==
|
||||
principalRequestedAmount) ==
|
||||
roundToReference(
|
||||
broker.asset(Number(2283105022831050, -18)),
|
||||
principalOutstandingAmount));
|
||||
principalRequestedAmount));
|
||||
BEAST_EXPECT(interest >= Number(0));
|
||||
|
||||
auto const rawPrincipal =
|
||||
@@ -1555,13 +1597,16 @@ class Loan_test : public beast::unit_test::suite
|
||||
state.paymentRemaining < 12 ||
|
||||
roundToReference(
|
||||
STAmount{broker.asset, rawPrincipal},
|
||||
principalOutstandingAmount) ==
|
||||
principalRequestedAmount) ==
|
||||
roundToReference(
|
||||
broker.asset(Number(8333228690659858, -14)),
|
||||
principalOutstandingAmount));
|
||||
principalRequestedAmount));
|
||||
BEAST_EXPECT(
|
||||
state.paymentRemaining > 1 ||
|
||||
rawPrincipal == state.principalOutstanding);
|
||||
auto const principal = roundToReference(
|
||||
STAmount{broker.asset, periodicPayment - interest},
|
||||
principalOutstandingAmount);
|
||||
principalRequestedAmount);
|
||||
BEAST_EXPECT(
|
||||
principal > Number(0) &&
|
||||
principal <= state.principalOutstanding);
|
||||
|
||||
@@ -75,14 +75,14 @@ template <AssetType A>
|
||||
Number
|
||||
loanTotalValueOutstanding(
|
||||
A asset,
|
||||
Number principalOutstanding,
|
||||
Number periodicPayment,
|
||||
Number const& originalPrincipal,
|
||||
Number const& periodicPayment,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return roundToAsset(
|
||||
asset,
|
||||
periodicPayment * paymentsRemaining,
|
||||
principalOutstanding,
|
||||
originalPrincipal,
|
||||
Number::upward);
|
||||
}
|
||||
|
||||
@@ -90,14 +90,15 @@ template <AssetType A>
|
||||
Number
|
||||
loanTotalValueOutstanding(
|
||||
A asset,
|
||||
Number principalOutstanding,
|
||||
Number const& originalPrincipal,
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
{
|
||||
return loanTotalValueOutstanding(
|
||||
asset,
|
||||
principalOutstanding,
|
||||
originalPrincipal,
|
||||
loanPeriodicPayment(
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
@@ -118,7 +119,8 @@ template <AssetType A>
|
||||
Number
|
||||
loanTotalInterestOutstanding(
|
||||
A asset,
|
||||
Number principalOutstanding,
|
||||
Number const& originalPrincipal,
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
@@ -127,6 +129,7 @@ loanTotalInterestOutstanding(
|
||||
principalOutstanding,
|
||||
loanTotalValueOutstanding(
|
||||
asset,
|
||||
originalPrincipal,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
@@ -160,6 +163,7 @@ template <AssetType A>
|
||||
PeriodicPayment
|
||||
computePeriodicPaymentParts(
|
||||
A const& asset,
|
||||
Number const& originalPrincipal,
|
||||
Number const& principalOutstanding,
|
||||
Number const& periodicPaymentAmount,
|
||||
Number const& periodicRate,
|
||||
@@ -171,19 +175,19 @@ computePeriodicPaymentParts(
|
||||
Number const interest = roundToAsset(
|
||||
asset,
|
||||
periodicPaymentAmount - principalOutstanding,
|
||||
principalOutstanding);
|
||||
originalPrincipal);
|
||||
return {interest, principalOutstanding};
|
||||
}
|
||||
Number const interest = roundToAsset(
|
||||
asset, principalOutstanding * periodicRate, principalOutstanding);
|
||||
asset, principalOutstanding * periodicRate, originalPrincipal);
|
||||
XRPL_ASSERT(
|
||||
interest >= 0,
|
||||
"ripple::detail::computePeriodicPayment : valid interest");
|
||||
|
||||
auto const roundedPayment =
|
||||
roundToAsset(asset, periodicPaymentAmount, principalOutstanding);
|
||||
roundToAsset(asset, periodicPaymentAmount, originalPrincipal);
|
||||
Number const principal =
|
||||
roundToAsset(asset, roundedPayment - interest, principalOutstanding);
|
||||
roundToAsset(asset, roundedPayment - interest, originalPrincipal);
|
||||
XRPL_ASSERT(
|
||||
principal > 0 && principal <= principalOutstanding,
|
||||
"ripple::detail::computePeriodicPayment : valid principal");
|
||||
@@ -203,18 +207,22 @@ template <AssetType A>
|
||||
Number
|
||||
valueMinusManagementFee(
|
||||
A const& asset,
|
||||
Number value,
|
||||
TenthBips32 managementFeeRate)
|
||||
Number const& value,
|
||||
TenthBips32 managementFeeRate,
|
||||
Number const& originalPrincipal)
|
||||
{
|
||||
return roundToAsset(
|
||||
asset, detail::minusManagementFee(value, managementFeeRate), value);
|
||||
asset,
|
||||
detail::minusManagementFee(value, managementFeeRate),
|
||||
originalPrincipal);
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanInterestOutstandingMinusFee(
|
||||
A const& asset,
|
||||
Number principalOutstanding,
|
||||
Number const& originalPrincipal,
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining,
|
||||
@@ -224,21 +232,24 @@ loanInterestOutstandingMinusFee(
|
||||
asset,
|
||||
detail::loanTotalInterestOutstanding(
|
||||
asset,
|
||||
originalPrincipal,
|
||||
principalOutstanding,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
managementFeeRate);
|
||||
managementFeeRate,
|
||||
originalPrincipal);
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanPeriodicPayment(
|
||||
A const& asset,
|
||||
Number principalOutstanding,
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 interestRate,
|
||||
std::uint32_t paymentInterval,
|
||||
std::uint32_t paymentsRemaining)
|
||||
std::uint32_t paymentsRemaining,
|
||||
Number const& originalPrincipal)
|
||||
{
|
||||
return roundToAsset(
|
||||
asset,
|
||||
@@ -247,18 +258,19 @@ loanPeriodicPayment(
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentsRemaining),
|
||||
principalOutstanding);
|
||||
originalPrincipal);
|
||||
}
|
||||
|
||||
template <AssetType A>
|
||||
Number
|
||||
loanLatePaymentInterest(
|
||||
A const& asset,
|
||||
Number principalOutstanding,
|
||||
Number const& principalOutstanding,
|
||||
TenthBips32 lateInterestRate,
|
||||
NetClock::time_point parentCloseTime,
|
||||
std::uint32_t startDate,
|
||||
std::uint32_t prevPaymentDate)
|
||||
std::uint32_t prevPaymentDate,
|
||||
Number const& originalPrincipal)
|
||||
{
|
||||
return roundToAsset(
|
||||
asset,
|
||||
@@ -268,7 +280,7 @@ loanLatePaymentInterest(
|
||||
parentCloseTime,
|
||||
startDate,
|
||||
prevPaymentDate),
|
||||
principalOutstanding);
|
||||
originalPrincipal);
|
||||
}
|
||||
|
||||
struct LoanPaymentParts
|
||||
@@ -288,6 +300,7 @@ loanComputePaymentParts(
|
||||
STAmount const& amount,
|
||||
beast::Journal j)
|
||||
{
|
||||
Number const originalPrincipalRequested = loan->at(sfPrincipalRequested);
|
||||
auto principalOutstandingField = loan->at(sfPrincipalOutstanding);
|
||||
bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment);
|
||||
|
||||
@@ -300,7 +313,7 @@ loanComputePaymentParts(
|
||||
Number const serviceFee = loan->at(sfLoanServiceFee);
|
||||
Number const latePaymentFee = loan->at(sfLatePaymentFee);
|
||||
Number const closePaymentFee = roundToAsset(
|
||||
asset, loan->at(sfClosePaymentFee), principalOutstandingField);
|
||||
asset, loan->at(sfClosePaymentFee), originalPrincipalRequested);
|
||||
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
|
||||
|
||||
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
|
||||
@@ -334,6 +347,7 @@ loanComputePaymentParts(
|
||||
|
||||
auto const periodic = detail::computePeriodicPaymentParts(
|
||||
asset,
|
||||
originalPrincipalRequested,
|
||||
principalOutstandingField,
|
||||
periodicPaymentAmount,
|
||||
periodicRate,
|
||||
@@ -341,7 +355,7 @@ loanComputePaymentParts(
|
||||
|
||||
Number const totalValueOutstanding = detail::loanTotalValueOutstanding(
|
||||
asset,
|
||||
principalOutstandingField,
|
||||
originalPrincipalRequested,
|
||||
periodicPaymentAmount,
|
||||
paymentRemainingField);
|
||||
XRPL_ASSERT(
|
||||
@@ -373,7 +387,8 @@ loanComputePaymentParts(
|
||||
lateInterestRate,
|
||||
view.parentCloseTime(),
|
||||
startDate,
|
||||
prevPaymentDateField);
|
||||
prevPaymentDateField,
|
||||
originalPrincipalRequested);
|
||||
XRPL_ASSERT(
|
||||
latePaymentInterest >= 0,
|
||||
"ripple::loanComputePaymentParts : valid late interest");
|
||||
@@ -420,7 +435,7 @@ loanComputePaymentParts(
|
||||
startDate,
|
||||
prevPaymentDateField,
|
||||
paymentInterval),
|
||||
principalOutstandingField);
|
||||
originalPrincipalRequested);
|
||||
XRPL_ASSERT(
|
||||
accruedInterest >= 0,
|
||||
"ripple::loanComputePaymentParts : valid accrued interest");
|
||||
@@ -428,7 +443,7 @@ loanComputePaymentParts(
|
||||
asset,
|
||||
tenthBipsOfValue(
|
||||
principalOutstandingField.value(), closeInterestRate),
|
||||
principalOutstandingField);
|
||||
originalPrincipalRequested);
|
||||
XRPL_ASSERT(
|
||||
closePrepaymentInterest >= 0,
|
||||
"ripple::loanComputePaymentParts : valid prepayment "
|
||||
@@ -468,7 +483,7 @@ loanComputePaymentParts(
|
||||
auto const totalDue = roundToAsset(
|
||||
asset,
|
||||
periodicPaymentAmount + serviceFee,
|
||||
principalOutstandingField,
|
||||
originalPrincipalRequested,
|
||||
Number::upward);
|
||||
|
||||
std::optional<NumberRoundModeGuard> mg(Number::downward);
|
||||
@@ -506,6 +521,7 @@ loanComputePaymentParts(
|
||||
if (!future)
|
||||
future = detail::computePeriodicPaymentParts(
|
||||
asset,
|
||||
originalPrincipalRequested,
|
||||
principalOutstandingField,
|
||||
periodicPaymentAmount,
|
||||
periodicRate,
|
||||
@@ -527,70 +543,70 @@ loanComputePaymentParts(
|
||||
|
||||
Number totalFeePaid = serviceFee * fullPeriodicPayments;
|
||||
|
||||
Number const newInterest = detail::loanTotalInterestOutstanding(
|
||||
asset,
|
||||
originalPrincipalRequested,
|
||||
principalOutstandingField,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentRemainingField) +
|
||||
totalInterestPaid;
|
||||
|
||||
Number overpaymentInterestPortion = 0;
|
||||
if (allowOverpayment)
|
||||
{
|
||||
Number const overpayment = std::min(
|
||||
principalOutstandingField.value(),
|
||||
amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid));
|
||||
|
||||
if (roundToAsset(asset, overpayment, principalOutstandingField) > 0)
|
||||
if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0)
|
||||
{
|
||||
Number const interestPortion = roundToAsset(
|
||||
asset,
|
||||
tenthBipsOfValue(overpayment, overpaymentInterestRate),
|
||||
principalOutstandingField);
|
||||
originalPrincipalRequested);
|
||||
Number const feePortion = roundToAsset(
|
||||
asset,
|
||||
tenthBipsOfValue(overpayment, overpaymentFee),
|
||||
principalOutstandingField);
|
||||
originalPrincipalRequested);
|
||||
Number const remainder = roundToAsset(
|
||||
asset,
|
||||
overpayment - interestPortion - feePortion,
|
||||
principalOutstandingField);
|
||||
originalPrincipalRequested);
|
||||
|
||||
// Don't process an overpayment if the whole amount (or more!) gets
|
||||
// eaten by fees
|
||||
if (remainder > 0)
|
||||
{
|
||||
overpaymentInterestPortion = interestPortion;
|
||||
totalPrincipalPaid += remainder;
|
||||
totalInterestPaid += interestPortion;
|
||||
totalFeePaid += feePortion;
|
||||
|
||||
principalOutstandingField -= remainder;
|
||||
|
||||
Number const newInterest = detail::loanTotalInterestOutstanding(
|
||||
asset,
|
||||
principalOutstandingField,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentRemainingField);
|
||||
|
||||
loanValueChange =
|
||||
(newInterest - totalInterestOutstanding) + interestPortion;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XRPL_ASSERT(
|
||||
loanValueChange <= 0,
|
||||
"ripple::loanComputePaymentParts : valid normal / overpayment "
|
||||
"value change");
|
||||
loanValueChange =
|
||||
(newInterest - totalInterestOutstanding) + overpaymentInterestPortion;
|
||||
|
||||
// Check the final results are rounded, to double-check that the
|
||||
// intermediate steps were rounded.
|
||||
XRPL_ASSERT(
|
||||
roundToAsset(asset, totalPrincipalPaid, principalOutstandingField) ==
|
||||
roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) ==
|
||||
totalPrincipalPaid,
|
||||
"ripple::loanComputePaymentParts : totalPrincipalPaid rounded");
|
||||
XRPL_ASSERT(
|
||||
roundToAsset(asset, totalInterestPaid, principalOutstandingField) ==
|
||||
roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) ==
|
||||
totalInterestPaid,
|
||||
"ripple::loanComputePaymentParts : totalInterestPaid rounded");
|
||||
XRPL_ASSERT(
|
||||
roundToAsset(asset, loanValueChange, principalOutstandingField) ==
|
||||
roundToAsset(asset, loanValueChange, originalPrincipalRequested) ==
|
||||
loanValueChange,
|
||||
"ripple::loanComputePaymentParts : loanValueChange rounded");
|
||||
XRPL_ASSERT(
|
||||
roundToAsset(asset, totalFeePaid, principalOutstandingField) ==
|
||||
roundToAsset(asset, totalFeePaid, originalPrincipalRequested) ==
|
||||
totalFeePaid,
|
||||
"ripple::loanComputePaymentParts : totalFeePaid rounded");
|
||||
return LoanPaymentParts{
|
||||
|
||||
@@ -1723,7 +1723,8 @@ NoModifiedUnmodifiableFields::finalize(
|
||||
fieldChanged(before, after, sfOverpaymentInterestRate) ||
|
||||
fieldChanged(before, after, sfStartDate) ||
|
||||
fieldChanged(before, after, sfPaymentInterval) ||
|
||||
fieldChanged(before, after, sfGracePeriod);
|
||||
fieldChanged(before, after, sfGracePeriod) ||
|
||||
fieldChanged(before, after, sfPrincipalRequested);
|
||||
break;
|
||||
default:
|
||||
/*
|
||||
|
||||
@@ -172,6 +172,7 @@ defaultLoan(
|
||||
{
|
||||
// Calculate the amount of the Default that First-Loss Capital covers:
|
||||
|
||||
Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested);
|
||||
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
|
||||
auto const totalDefaultAmount = principalOutstanding + interestOutstanding;
|
||||
@@ -192,7 +193,7 @@ defaultLoan(
|
||||
brokerDebtTotalProxy.value(), coverRateMinimum),
|
||||
coverRateLiquidation),
|
||||
defaultAmount),
|
||||
std::max(brokerDebtTotalProxy.value(), defaultAmount));
|
||||
originalPrincipalRequested);
|
||||
auto const returnToVault = defaultCovered + loanAssetsAvailableProxy;
|
||||
auto const vaultDefaultAmount = defaultAmount - defaultCovered;
|
||||
|
||||
@@ -373,6 +374,7 @@ LoanManage::doApply()
|
||||
auto const vaultAsset = vaultSle->at(sfAsset);
|
||||
|
||||
TenthBips32 const interestRate{loanSle->at(sfInterestRate)};
|
||||
Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested);
|
||||
auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
|
||||
|
||||
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
|
||||
@@ -380,6 +382,7 @@ LoanManage::doApply()
|
||||
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
|
||||
auto const interestOutstanding = loanInterestOutstandingMinusFee(
|
||||
vaultAsset,
|
||||
originalPrincipalRequested,
|
||||
principalOutstanding.value(),
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
|
||||
@@ -177,8 +177,7 @@ LoanPay::doApply()
|
||||
|
||||
//------------------------------------------------------
|
||||
// Loan object state changes
|
||||
auto const originalPrincipalOutstanding =
|
||||
loanSle->at(sfPrincipalOutstanding);
|
||||
Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested);
|
||||
|
||||
view.update(loanSle);
|
||||
|
||||
@@ -212,7 +211,7 @@ LoanPay::doApply()
|
||||
auto const managementFee = roundToAsset(
|
||||
asset,
|
||||
tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate),
|
||||
originalPrincipalOutstanding);
|
||||
originalPrincipalRequested);
|
||||
|
||||
auto const totalPaidToVault = paymentParts->principalPaid +
|
||||
paymentParts->interestPaid - managementFee;
|
||||
@@ -227,7 +226,7 @@ LoanPay::doApply()
|
||||
bool const sufficientCover = coverAvailableField >=
|
||||
roundToAsset(asset,
|
||||
tenthBipsOfValue(debtTotalField.value(), coverRateMinimum),
|
||||
originalPrincipalOutstanding);
|
||||
originalPrincipalRequested);
|
||||
if (!sufficientCover)
|
||||
{
|
||||
// Add the fee to to First Loss Cover Pool
|
||||
@@ -237,9 +236,17 @@ LoanPay::doApply()
|
||||
// Decrease LoanBroker Debt by the amount paid, add the Loan value change,
|
||||
// and subtract the change in the management fee
|
||||
auto const vaultValueChange = valueMinusManagementFee(
|
||||
asset, paymentParts->valueChange, managementFeeRate);
|
||||
asset,
|
||||
paymentParts->valueChange,
|
||||
managementFeeRate,
|
||||
originalPrincipalRequested);
|
||||
// debtDecrease may be negative, increasing the debt
|
||||
auto const debtDecrease = totalPaidToVault - vaultValueChange;
|
||||
XRPL_ASSERT_PARTS(
|
||||
roundToAsset(asset, debtDecrease, originalPrincipalRequested) ==
|
||||
debtDecrease,
|
||||
"ripple::LoanPay::doApply",
|
||||
"debtDecrease rounding good");
|
||||
if (debtDecrease >= debtTotalField)
|
||||
debtTotalField = 0;
|
||||
else
|
||||
|
||||
@@ -363,6 +363,7 @@ LoanSet::doApply()
|
||||
auto const loanInterestToVault = loanInterestOutstandingMinusFee(
|
||||
vaultAsset,
|
||||
principalRequested,
|
||||
principalRequested,
|
||||
interestRate,
|
||||
paymentInterval,
|
||||
paymentTotal,
|
||||
@@ -382,6 +383,7 @@ LoanSet::doApply()
|
||||
};
|
||||
|
||||
// Set required tx fields and pre-computed fields
|
||||
loan->at(sfPrincipalRequested) = principalRequested;
|
||||
loan->at(sfPrincipalOutstanding) = principalRequested;
|
||||
loan->at(sfStartDate) = startDate;
|
||||
loan->at(sfPaymentInterval) = paymentInterval;
|
||||
|
||||
Reference in New Issue
Block a user