Fix or work around loan payment rounding problems

This commit is contained in:
Ed Hennis
2025-05-15 19:16:58 +01:00
parent 2d767f2733
commit dc7033805b
9 changed files with 197 additions and 109 deletions

View File

@@ -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});
}
//------------------------------------------------------------------------------

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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{

View File

@@ -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:
/*

View File

@@ -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,

View File

@@ -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

View File

@@ -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;