diff --git a/include/xrpl/protocol/STAmount.h b/include/xrpl/protocol/STAmount.h index d3f67da4af..560059e49a 100644 --- a/include/xrpl/protocol/STAmount.h +++ b/include/xrpl/protocol/STAmount.h @@ -66,15 +66,15 @@ public: static int const cMaxOffset = 80; // Maximum native value supported by the code - static std::uint64_t const cMinValue = 1000000000000000ull; - static std::uint64_t const cMaxValue = 9999999999999999ull; - static std::uint64_t const cMaxNative = 9000000000000000000ull; + static std::uint64_t const cMinValue = 1'000'000'000'000'000ull; + static std::uint64_t const cMaxValue = 9'999'999'999'999'999ull; + static std::uint64_t const cMaxNative = 9'000'000'000'000'000'000ull; // Max native value on network. - static std::uint64_t const cMaxNativeN = 100000000000000000ull; - static std::uint64_t const cIssuedCurrency = 0x8000000000000000ull; - static std::uint64_t const cPositive = 0x4000000000000000ull; - static std::uint64_t const cMPToken = 0x2000000000000000ull; + static std::uint64_t const cMaxNativeN = 100'000'000'000'000'000ull; + static std::uint64_t const cIssuedCurrency = 0x8'000'000'000'000'000ull; + static std::uint64_t const cPositive = 0x4'000'000'000'000'000ull; + static std::uint64_t const cMPToken = 0x2'000'000'000'000'000ull; static std::uint64_t const cValueMask = ~(cPositive | cMPToken); static std::uint64_t const uRateOne; @@ -695,21 +695,22 @@ divRoundStrict( std::uint64_t getRate(STAmount const& offerOut, STAmount const& offerIn); -/** Round an arbitrary precision Amount to the precision of a reference Amount. +/** Round an arbitrary precision Amount to the precision of an STAmount that has + * a given exponent. * * This is used to ensure that calculations involving IOU amounts do not collect * dust beyond the precision of the reference value. * * @param value The value to be rounded - * @param referenceValue A reference value to establish the precision limit of - * `value`. Should be larger than `value`. + * @param scale An exponent value to establish the precision limit of + * `value`. Should be larger than `value.exponent()`. * @param rounding Optional Number rounding mode * */ STAmount -roundToReference( - STAmount const value, - STAmount referenceValue, +roundToScale( + STAmount value, + std::int32_t scale, Number::rounding_mode rounding = Number::getround()); /** Round an arbitrary precision Number to the precision of a given Asset. @@ -720,9 +721,8 @@ roundToReference( * * @param asset The relevant asset * @param value The value to be rounded - * @param referenceValue Only relevant to IOU assets. A reference value to - * establish the precision limit of `value`. Should be larger than - * `value`. + * @param scale Only relevant to IOU assets. An exponent value to establish the + * precision limit of `value`. Should be larger than `value.exponent()`. * @param rounding Optional Number rounding mode */ template @@ -730,7 +730,7 @@ Number roundToAsset( A const& asset, Number const& value, - Number const& referenceValue, + std::int32_t scale, Number::rounding_mode rounding = Number::getround()) { NumberRoundModeGuard mg(rounding); @@ -739,7 +739,7 @@ roundToAsset( return ret; // Not that the ctor will round integral types (XRP, MPT) via canonicalize, // so no extra work is needed for those. - return roundToReference(ret, STAmount{asset, referenceValue}); + return roundToScale(ret, scale); } //------------------------------------------------------------------------------ diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index bfecd0c92f..99e16e6b70 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -548,25 +548,29 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfLoanBrokerID, soeREQUIRED}, {sfLoanSequence, soeREQUIRED}, {sfBorrower, soeREQUIRED}, - {sfLoanOriginationFee, soeREQUIRED}, - {sfLoanServiceFee, soeREQUIRED}, - {sfLatePaymentFee, soeREQUIRED}, - {sfClosePaymentFee, soeREQUIRED}, - {sfOverpaymentFee, soeREQUIRED}, - {sfInterestRate, soeREQUIRED}, - {sfLateInterestRate, soeREQUIRED}, - {sfCloseInterestRate, soeREQUIRED}, - {sfOverpaymentInterestRate, soeREQUIRED}, + {sfLoanOriginationFee, soeDEFAULT}, + {sfLoanServiceFee, soeDEFAULT}, + {sfLatePaymentFee, soeDEFAULT}, + {sfClosePaymentFee, soeDEFAULT}, + {sfOverpaymentFee, soeDEFAULT}, + {sfInterestRate, soeDEFAULT}, + {sfLateInterestRate, soeDEFAULT}, + {sfCloseInterestRate, soeDEFAULT}, + {sfOverpaymentInterestRate, soeDEFAULT}, {sfStartDate, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED}, {sfGracePeriod, soeREQUIRED}, + {sfPeriodicPayment, soeREQUIRED}, {sfPreviousPaymentDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED}, {sfPrincipalOutstanding, soeREQUIRED}, - // Save the original request amount for rounding / scaling of - // other computations, particularly for IOUs - {sfPrincipalRequested, soeREQUIRED}, + {sfTotalValueOutstanding, soeDEFAULT}, + // Based on the original principal borrowed, used for + // rounding calculated values so they are all on a + // consistent scale - that is, they all have the same + // number of decimal places after the decimal point. + {sfLoanScale, soeDEFAULT}, })) #undef EXPAND diff --git a/include/xrpl/protocol/detail/sfields.macro b/include/xrpl/protocol/detail/sfields.macro index af1429461f..3c3dda69e0 100644 --- a/include/xrpl/protocol/detail/sfields.macro +++ b/include/xrpl/protocol/detail/sfields.macro @@ -239,12 +239,11 @@ TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11) TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) +TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15) +TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16) // int32 -// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 -// in this file and has been defined here for test only. -// TODO: Replace `sfDummyInt32` with actually useful field. -TYPED_SFIELD(sfDummyInt32, INT32, 1) // for tests only +TYPED_SFIELD(sfLoanScale, INT32, 1) // currency amount (common) TYPED_SFIELD(sfAmount, AMOUNT, 1) diff --git a/src/libxrpl/protocol/STAmount.cpp b/src/libxrpl/protocol/STAmount.cpp index 15524d605a..74f4576831 100644 --- a/src/libxrpl/protocol/STAmount.cpp +++ b/src/libxrpl/protocol/STAmount.cpp @@ -1510,32 +1510,28 @@ canonicalizeRoundStrict( } STAmount -roundToReference( - STAmount const value, - STAmount referenceValue, - Number::rounding_mode rounding) +roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding) { // Nothing to do for intgral types. if (value.asset().native() || !value.asset().holds()) return value; - // If the value is greater than or equal to the referenceValue (ignoring - // sign), then rounding will do nothing, so just return the value. - if (value.exponent() > referenceValue.exponent() || - (value.exponent() == referenceValue.exponent() && - value.mantissa() >= referenceValue.mantissa())) + // If the value's exponent is greater than or equal to the scale, then + // rounding will do nothing, and might even lose precision, so just return + // the value. + if (value.exponent() >= scale) return value; - if (referenceValue.negative() != value.negative()) - referenceValue.negate(); + STAmount referenceValue{ + value.asset(), STAmount::cMinValue, scale, value.negative()}; NumberRoundModeGuard mg(rounding); // With an IOU, the total will be truncated to the precision of the // larger value: referenceValue - STAmount const total = referenceValue + value; + value += referenceValue; // Remove the reference value, and we're left with the rounded value. - STAmount const result = total - referenceValue; - return result; + value -= referenceValue; + return value; } namespace { diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 84e45e4ad5..8535ed95f5 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -111,7 +111,7 @@ class Loan_test : public beast::unit_test::suite NetClock::time_point startDate = {}; std::uint32_t nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; - Number const principalRequested = 0; + std::int32_t const loanScale = 0; Number principalOutstanding = 0; std::uint32_t flags = 0; std::uint32_t paymentInterval = 0; @@ -221,7 +221,7 @@ class Loan_test : public beast::unit_test::suite std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, Number const& principalRequested, - Number const& principalOutstanding, + Number const& loanScale, std::uint32_t flags) const { using namespace jtx; @@ -233,13 +233,12 @@ class Loan_test : public beast::unit_test::suite loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT( - loan->at(sfPrincipalRequested) == principalRequested); + env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale); env.test.BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == principalOutstanding); env.test.BEAST_EXPECT( - loan->at(sfPrincipalRequested) == - broker.asset(loanAmount).value()); + loan->at(sfLoanScale) == + broker.asset(loanAmount).value().exponent()); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; @@ -369,7 +368,7 @@ class Loan_test : public beast::unit_test::suite .startDate = tp{d{loan->at(sfStartDate)}}, .nextPaymentDate = loan->at(sfNextPaymentDueDate), .paymentRemaining = loan->at(sfPaymentRemaining), - .principalRequested = loan->at(sfPrincipalRequested), + .loanScale = loan->at(sfLoanScale), .principalOutstanding = loan->at(sfPrincipalOutstanding), .flags = loan->at(sfFlags), .paymentInterval = loan->at(sfPaymentInterval), @@ -611,7 +610,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( loan->at(sfNextPaymentDueDate) == startDate + interval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest); + BEAST_EXPECT(loan->at(sfLoanScale) == principalRequest.exponent()); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); } @@ -1991,7 +1990,7 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan[sfPaymentRemaining] == 1); BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); - BEAST_EXPECT(loan[sfPrincipalRequested] == "1000000000"); + BEAST_EXPECT(loan[sfLoanScale] == 0); BEAST_EXPECT( loan[sfStartDate].asUInt() == startDate.time_since_epoch().count()); @@ -2183,7 +2182,7 @@ class Loan_test : public beast::unit_test::suite { // Verify the payment decreased the principal BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent()); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); } @@ -2196,7 +2195,7 @@ class Loan_test : public beast::unit_test::suite { // Verify the payment decreased the principal BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments - 1); - BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); + BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent()); BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); } diff --git a/src/test/protocol/STParsedJSON_test.cpp b/src/test/protocol/STParsedJSON_test.cpp index 1e1e1fb9f4..12408c272e 100644 --- a/src/test/protocol/STParsedJSON_test.cpp +++ b/src/test/protocol/STParsedJSON_test.cpp @@ -743,63 +743,63 @@ class STParsedJSON_test : public beast::unit_test::suite { Json::Value j; int const minInt32 = -2147483648; - j[sfDummyInt32] = minInt32; + j[sfLoanScale] = minInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == minInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == minInt32); } // max value { Json::Value j; int const maxInt32 = 2147483647; - j[sfDummyInt32] = maxInt32; + j[sfLoanScale] = maxInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == maxInt32); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == maxInt32); } // max uint value { Json::Value j; unsigned int const maxUInt32 = 2147483647u; - j[sfDummyInt32] = maxUInt32; + j[sfLoanScale] = maxUInt32; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) BEAST_EXPECT( - obj.object->getFieldI32(sfDummyInt32) == + obj.object->getFieldI32(sfLoanScale) == static_cast(maxUInt32)); } // Test with string value { Json::Value j; - j[sfDummyInt32] = "2147483647"; + j[sfLoanScale] = "2147483647"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) BEAST_EXPECT( - obj.object->getFieldI32(sfDummyInt32) == 2147483647u); + obj.object->getFieldI32(sfLoanScale) == 2147483647u); } // Test with string negative value { Json::Value j; int value = -2147483648; - j[sfDummyInt32] = std::to_string(value); + j[sfLoanScale] = std::to_string(value); STParsedJSONObject obj("Test", j); BEAST_EXPECT(obj.object.has_value()); - if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) - BEAST_EXPECT(obj.object->getFieldI32(sfDummyInt32) == value); + if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale))) + BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == value); } // Test out of range value for int32 (negative) { Json::Value j; - j[sfDummyInt32] = "-2147483649"; + j[sfLoanScale] = "-2147483649"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -807,7 +807,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test out of range value for int32 (positive) { Json::Value j; - j[sfDummyInt32] = 2147483648u; + j[sfLoanScale] = 2147483648u; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -815,7 +815,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test string value out of range { Json::Value j; - j[sfDummyInt32] = "2147483648"; + j[sfLoanScale] = "2147483648"; STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -823,7 +823,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (arrayValue) { Json::Value j; - j[sfDummyInt32] = Json::Value(Json::arrayValue); + j[sfLoanScale] = Json::Value(Json::arrayValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } @@ -831,7 +831,7 @@ class STParsedJSON_test : public beast::unit_test::suite // Test bad_type (objectValue) { Json::Value j; - j[sfDummyInt32] = Json::Value(Json::objectValue); + j[sfLoanScale] = Json::Value(Json::objectValue); STParsedJSONObject obj("Test", j); BEAST_EXPECT(!obj.object.has_value()); } diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index cea123d75d..5f28ed9120 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -40,22 +40,100 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval); Number loanPeriodicPayment( - Number principalOutstanding, - Number periodicRate, + Number const& principalOutstanding, + Number const& periodicRate, std::uint32_t paymentsRemaining); Number loanPeriodicPayment( - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining); +Number +loanLatePaymentInterest( + Number const& principalOutstanding, + TenthBips32 lateInterestRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate); + +Number +loanAccruedInterest( + Number const& principalOutstanding, + Number const& periodicRate, + NetClock::time_point parentCloseTime, + std::uint32_t startDate, + std::uint32_t prevPaymentDate, + std::uint32_t paymentInterval); + +inline Number +minusManagementFee(Number const& value, TenthBips32 managementFeeRate) +{ + return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); +} + +} // namespace detail + +template +Number +valueMinusManagementFee( + A const& asset, + Number const& value, + TenthBips32 managementFeeRate, + std::int32_t scale) +{ + return roundToAsset( + asset, detail::minusManagementFee(value, managementFeeRate), scale); +} + +Number +loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) +{ + return detail::loanPeriodicRate(interestRate, paymentInterval); +} + +template +Number +loanPeriodicPayment( + A const& asset, + Number const& principalOutstanding, + Number const& periodicRate, + std::uint32_t paymentsRemaining, + std::int32_t scale) +{ + return roundToAsset( + asset, + detail::loanPeriodicPayment( + principalOutstanding, periodicRate, paymentsRemaining), + scale, + Number::upward); +} + +template +Number +loanPeriodicPayment( + A const& asset, + Number const& principalOutstanding, + TenthBips32 interestRate, + std::uint32_t paymentInterval, + std::uint32_t paymentsRemaining, + std::int32_t scale) +{ + loanPeriodicPayment( + asset, + principalOutstanding, + loanPeriodicRate(interestRate, paymentInterval), + paymentsRemaining, + scale); +} + template Number loanTotalValueOutstanding( A asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& periodicPayment, std::uint32_t paymentsRemaining) { @@ -66,7 +144,7 @@ loanTotalValueOutstanding( * Value Calculation), specifically "totalValueOutstanding = ..." */ periodicPayment * paymentsRemaining, - originalPrincipal, + scale, Number::upward); } @@ -74,7 +152,7 @@ template Number loanTotalValueOutstanding( A asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, @@ -86,19 +164,21 @@ loanTotalValueOutstanding( */ return loanTotalValueOutstanding( asset, - originalPrincipal, + scale, loanPeriodicPayment( + asset, principalOutstanding, interestRate, paymentInterval, - paymentsRemaining), + paymentsRemaining, + scale), paymentsRemaining); } inline Number loanTotalInterestOutstanding( - Number principalOutstanding, - Number totalValueOutstanding) + Number const& principalOutstanding, + Number const& totalValueOutstanding) { /* * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan @@ -111,7 +191,7 @@ template Number loanTotalInterestOutstanding( A asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, @@ -125,94 +205,47 @@ loanTotalInterestOutstanding( principalOutstanding, loanTotalValueOutstanding( asset, - originalPrincipal, + scale, principalOutstanding, interestRate, paymentInterval, paymentsRemaining)); } -Number -loanLatePaymentInterest( - Number principalOutstanding, - TenthBips32 lateInterestRate, - NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate); - -Number -loanAccruedInterest( - Number principalOutstanding, - Number periodicRate, - NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate, - std::uint32_t paymentInterval); - -inline Number -minusManagementFee(Number value, TenthBips32 managementFeeRate) -{ - return tenthBipsOfValue(value, tenthBipsPerUnity - managementFeeRate); -} - -} // namespace detail - template Number -valueMinusManagementFee( +loanInterestOutstandingMinusFee( A const& asset, - Number const& value, + Number const& totalInterestOutstanding, TenthBips32 managementFeeRate, - Number const& originalPrincipal) + std::int32_t scale) { - return roundToAsset( - asset, - detail::minusManagementFee(value, managementFeeRate), - originalPrincipal); + return valueMinusManagementFee( + asset, totalInterestOutstanding, managementFeeRate, scale); } template Number loanInterestOutstandingMinusFee( A const& asset, - Number const& originalPrincipal, + std::int32_t scale, Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate) { - return valueMinusManagementFee( + return loanInterestOutstandingMinusFee( asset, - detail::loanTotalInterestOutstanding( + loanTotalInterestOutstanding( asset, - originalPrincipal, + scale, principalOutstanding, interestRate, paymentInterval, paymentsRemaining), managementFeeRate, - originalPrincipal); -} - -template -Number -loanPeriodicPayment( - A const& asset, - Number const& principalOutstanding, - TenthBips32 interestRate, - std::uint32_t paymentInterval, - std::uint32_t paymentsRemaining, - Number const& originalPrincipal) -{ - return roundToAsset( - asset, - detail::loanPeriodicPayment( - principalOutstanding, - interestRate, - paymentInterval, - paymentsRemaining), - originalPrincipal); + scale); } template @@ -224,7 +257,7 @@ loanLatePaymentInterest( NetClock::time_point parentCloseTime, std::uint32_t startDate, std::uint32_t prevPaymentDate, - Number const& originalPrincipal) + Number const& scale) { return roundToAsset( asset, @@ -234,7 +267,15 @@ loanLatePaymentInterest( parentCloseTime, startDate, prevPaymentDate), - originalPrincipal); + scale); +} + +template +bool +rounded(A const& asset, Number const& value, std::int32_t scale) +{ + return roundToAsset(asset, value, scale, Number::downward) == value && + roundToAsset(asset, value, scale, Number::upward) == value; } struct PaymentParts @@ -246,9 +287,10 @@ struct PaymentParts template PaymentParts -computePeriodicPaymentParts( +computePaymentParts( A const& asset, - Number const& originalPrincipal, + std::int32_t scale, + Number const& totalValueOutstanding, Number const& principalOutstanding, Number const& periodicPaymentAmount, Number const& serviceFee, @@ -259,16 +301,17 @@ computePeriodicPaymentParts( * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular * Payment) */ - Number const roundedFee = - roundToAsset(asset, serviceFee, originalPrincipal); - if (paymentRemaining == 1) + XRPL_ASSERT_PARTS( + rounded(asset, totalValueOutstanding, scale) && + rounded(asset, principalOutstanding, scale) && + rounded(asset, periodicPaymentAmount, scale), + "ripple::computePaymentParts", + "Asset values are rounded"); + Number const roundedFee = roundToAsset(asset, serviceFee, scale); + if (paymentRemaining == 1 || periodicPaymentAmount > totalValueOutstanding) { // If there's only one payment left, we need to pay off the principal. - Number const interest = roundToAsset( - asset, - periodicPaymentAmount - principalOutstanding, - originalPrincipal, - Number::upward); + Number const interest = totalValueOutstanding - principalOutstanding; return { .interest = interest, .principal = principalOutstanding, @@ -284,10 +327,7 @@ computePeriodicPaymentParts( * Because those values deal with funds, they need to be rounded. */ Number const interest = roundToAsset( - asset, - principalOutstanding * periodicRate, - originalPrincipal, - Number::upward); + asset, principalOutstanding * periodicRate, scale, Number::upward); XRPL_ASSERT( interest >= 0, "ripple::detail::computePeriodicPayment : valid interest"); @@ -296,8 +336,8 @@ computePeriodicPaymentParts( // payment amount, ensuring that some principal is paid regardless of any // other results. auto const roundedPayment = [&]() { - auto roundedPayment = roundToAsset( - asset, periodicPaymentAmount, originalPrincipal, Number::upward); + auto roundedPayment = + roundToAsset(asset, periodicPaymentAmount, scale, Number::upward); if (roundedPayment > interest) return roundedPayment; auto newPayment = roundedPayment; @@ -310,21 +350,21 @@ computePeriodicPaymentParts( { // Non-integral types: IOU. Add "dust" that will not be lost in // rounding. - auto const epsilon = Number{1, originalPrincipal.exponent() - 14}; + auto const epsilon = Number{1, scale - 14}; newPayment += epsilon; } - roundedPayment = roundToAsset(asset, newPayment, originalPrincipal); + roundedPayment = roundToAsset(asset, newPayment, scale); XRPL_ASSERT_PARTS( roundedPayment == newPayment, - "ripple::computePeriodicPaymentParts", + "ripple::computePaymentParts", "epsilon preserved in rounding"); return roundedPayment; }(); Number const principal = - roundToAsset(asset, roundedPayment - interest, originalPrincipal); + roundToAsset(asset, roundedPayment - interest, scale); XRPL_ASSERT_PARTS( principal > 0 && principal <= principalOutstanding, - "ripple::computePeriodicPaymentParts", + "ripple::computePaymentParts", "valid principal"); return {.interest = interest, .principal = principal, .fee = roundedFee}; @@ -375,7 +415,7 @@ handleLatePayment( std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const lateInterestRate, - Number const& originalPrincipalRequested, + std::int32_t loanScale, Number const& latePaymentFee, STAmount const& amount, beast::Journal j) @@ -393,7 +433,7 @@ handleLatePayment( view.parentCloseTime(), startDate, prevPaymentDateProxy, - originalPrincipalRequested); + loanScale); XRPL_ASSERT( latePaymentInterest >= 0, "ripple::handleLatePayment : valid late interest"); @@ -446,7 +486,7 @@ handleFullPayment( std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const closeInterestRate, - Number const& originalPrincipalRequested, + std::int32_t loanScale, Number const& totalInterestOutstanding, Number const& periodicRate, Number const& closePaymentFee, @@ -468,14 +508,14 @@ handleFullPayment( startDate, prevPaymentDateProxy, paymentInterval), - originalPrincipalRequested); + loanScale); XRPL_ASSERT( accruedInterest >= 0, "ripple::handleFullPayment : valid accrued interest"); auto const prepaymentPenalty = roundToAsset( asset, tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), - originalPrincipalRequested); + loanScale); XRPL_ASSERT( prepaymentPenalty >= 0, "ripple::handleFullPayment : valid prepayment " @@ -521,7 +561,8 @@ loanMakePayment( * This function is an implementation of the XLS-66 spec, * section 3.2.4.3 (Transaction Pseudo-code) */ - Number const originalPrincipalRequested = loan->at(sfPrincipalRequested); + std::int32_t const loanScale = loan->at(sfLoanScale); + auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); @@ -533,8 +574,8 @@ loanMakePayment( Number const serviceFee = loan->at(sfLoanServiceFee); Number const latePaymentFee = loan->at(sfLatePaymentFee); - Number const closePaymentFee = roundToAsset( - asset, loan->at(sfClosePaymentFee), originalPrincipalRequested); + Number const closePaymentFee = + roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); @@ -567,26 +608,23 @@ loanMakePayment( periodicPaymentAmount > 0, "ripple::computePeriodicPayment : valid payment"); - auto const periodic = computePeriodicPaymentParts( + auto const periodic = computePaymentParts( asset, - originalPrincipalRequested, + loanScale, + totalValueOutstandingProxy, principalOutstandingProxy, periodicPaymentAmount, serviceFee, periodicRate, paymentRemainingProxy); - Number const totalValueOutstanding = detail::loanTotalValueOutstanding( - asset, - originalPrincipalRequested, - periodicPaymentAmount, - paymentRemainingProxy); + Number const totalValueOutstanding = loanTotalValueOutstanding( + asset, loanScale, periodicPaymentAmount, paymentRemainingProxy); XRPL_ASSERT( totalValueOutstanding > 0, "ripple::loanMakePayment : valid total value"); - Number const totalInterestOutstanding = - detail::loanTotalInterestOutstanding( - principalOutstandingProxy, totalValueOutstanding); + Number const totalInterestOutstanding = loanTotalInterestOutstanding( + principalOutstandingProxy, totalValueOutstanding); XRPL_ASSERT_PARTS( totalInterestOutstanding >= 0, "ripple::loanMakePayment", @@ -612,7 +650,7 @@ loanMakePayment( startDate, paymentInterval, lateInterestRate, - originalPrincipalRequested, + loanScale, latePaymentFee, amount, j)) @@ -631,7 +669,7 @@ loanMakePayment( startDate, paymentInterval, closeInterestRate, - originalPrincipalRequested, + loanScale, totalInterestOutstanding, periodicRate, closePaymentFee, @@ -682,9 +720,10 @@ loanMakePayment( { // Only do the work if we need to if (!future) - future = computePeriodicPaymentParts( + future = computePaymentParts( asset, - originalPrincipalRequested, + loanScale, + totalValueOutstandingProxy, principalOutstandingProxy, periodicPaymentAmount, serviceFee, @@ -707,9 +746,9 @@ loanMakePayment( Number totalFeePaid = serviceFee * fullPeriodicPayments; - Number const newInterest = detail::loanTotalInterestOutstanding( + Number const newInterest = loanTotalInterestOutstanding( asset, - originalPrincipalRequested, + loanScale, principalOutstandingProxy, interestRate, paymentInterval, @@ -725,20 +764,18 @@ loanMakePayment( principalOutstandingProxy.value(), amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); - if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0) + if (roundToAsset(asset, overpayment, loanScale) > 0) { Number const interestPortion = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentInterestRate), - originalPrincipalRequested); + loanScale); Number const feePortion = roundToAsset( asset, tenthBipsOfValue(overpayment, overpaymentFee), - originalPrincipalRequested); + loanScale); Number const remainder = roundToAsset( - asset, - overpayment - interestPortion - feePortion, - originalPrincipalRequested); + asset, overpayment - interestPortion - feePortion, loanScale); // Don't process an overpayment if the whole amount (or more!) // gets eaten by fees @@ -760,20 +797,17 @@ loanMakePayment( // Check the final results are rounded, to double-check that the // intermediate steps were rounded. XRPL_ASSERT( - roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) == + roundToAsset(asset, totalPrincipalPaid, loanScale) == totalPrincipalPaid, "ripple::loanMakePayment : totalPrincipalPaid rounded"); XRPL_ASSERT( - roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) == - totalInterestPaid, + roundToAsset(asset, totalInterestPaid, loanScale) == totalInterestPaid, "ripple::loanMakePayment : totalInterestPaid rounded"); XRPL_ASSERT( - roundToAsset(asset, loanValueChange, originalPrincipalRequested) == - loanValueChange, + roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, "ripple::loanMakePayment : loanValueChange rounded"); XRPL_ASSERT( - roundToAsset(asset, totalFeePaid, originalPrincipalRequested) == - totalFeePaid, + roundToAsset(asset, totalFeePaid, loanScale) == totalFeePaid, "ripple::loanMakePayment : totalFeePaid rounded"); return LoanPaymentParts{ .principalPaid = totalPrincipalPaid, diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index 66d88728e5..c56e05acff 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -48,8 +48,8 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval) Number loanPeriodicPayment( - Number principalOutstanding, - Number periodicRate, + Number const& principalOutstanding, + Number const& periodicRate, std::uint32_t paymentsRemaining) { if (principalOutstanding == 0 || paymentsRemaining == 0) @@ -72,7 +72,7 @@ loanPeriodicPayment( Number loanPeriodicPayment( - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining) @@ -91,7 +91,7 @@ loanPeriodicPayment( Number loanLatePaymentInterest( - Number principalOutstanding, + Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, std::uint32_t startDate, @@ -114,8 +114,8 @@ loanLatePaymentInterest( Number loanAccruedInterest( - Number principalOutstanding, - Number periodicRate, + Number const& principalOutstanding, + Number const& periodicRate, NetClock::time_point parentCloseTime, std::uint32_t startDate, std::uint32_t prevPaymentDate, diff --git a/src/xrpld/app/tx/detail/InvariantCheck.cpp b/src/xrpld/app/tx/detail/InvariantCheck.cpp index fa43579bd5..2cb03181dd 100644 --- a/src/xrpld/app/tx/detail/InvariantCheck.cpp +++ b/src/xrpld/app/tx/detail/InvariantCheck.cpp @@ -2203,7 +2203,7 @@ NoModifiedUnmodifiableFields::finalize( fieldChanged(before, after, sfStartDate) || fieldChanged(before, after, sfPaymentInterval) || fieldChanged(before, after, sfGracePeriod) || - fieldChanged(before, after, sfPrincipalRequested); + fieldChanged(before, after, sfLoanScale); break; default: /* diff --git a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp index 0505218dcb..894e69cac8 100644 --- a/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp +++ b/src/xrpld/app/tx/detail/LoanBrokerCoverWithdraw.cpp @@ -146,7 +146,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx) vaultAsset, tenthBipsOfValue( currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), - currentDebtTotal); + currentDebtTotal.exponent()); if (coverAvail < amount) return tecINSUFFICIENT_FUNDS; if ((coverAvail - amount) < minimumCover) diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 99622d26ed..ac5da7f7dc 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -145,7 +145,7 @@ LoanManage::defaultLoan( { // Calculate the amount of the Default that First-Loss Capital covers: - Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + std::int32_t const loanScale = loanSle->at(sfLoanScale); TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto const totalDefaultAmount = principalOutstanding + interestOutstanding; @@ -162,7 +162,7 @@ LoanManage::defaultLoan( brokerDebtTotalProxy.value(), coverRateMinimum), coverRateLiquidation), totalDefaultAmount), - originalPrincipalRequested); + loanScale); auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; @@ -380,7 +380,7 @@ LoanManage::doApply() auto const vaultAsset = vaultSle->at(sfAsset); TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + std::int32_t const loanScale = loanSle->at(sfLoanScale); auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; @@ -388,7 +388,7 @@ LoanManage::doApply() auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); auto const interestOutstanding = loanInterestOutstandingMinusFee( vaultAsset, - originalPrincipalRequested, + loanScale, principalOutstanding.value(), interestRate, paymentInterval, diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 747664fbaf..71e73eeb07 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -147,7 +147,7 @@ LoanPay::doApply() //------------------------------------------------------ // Loan object state changes - Number const originalPrincipalRequested = loanSle->at(sfPrincipalRequested); + std::int32_t const loanScale = loanSle->at(sfLoanScale); // Unimpair the loan if it was impaired. Do this before the payment is // attempted, so the original values can be used. If the payment fails, this @@ -163,7 +163,7 @@ LoanPay::doApply() auto const interestOutstanding = loanInterestOutstandingMinusFee( asset, - originalPrincipalRequested, + loanScale, principalOutstanding.value(), interestRate, paymentInterval, @@ -219,7 +219,7 @@ LoanPay::doApply() auto const managementFee = roundToAsset( asset, tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), - originalPrincipalRequested); + loanScale); auto const totalPaidToVault = paymentParts->principalPaid + paymentParts->interestPaid - managementFee; @@ -241,7 +241,7 @@ LoanPay::doApply() bool const sufficientCover = coverAvailableField >= roundToAsset(asset, tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), - originalPrincipalRequested); + loanScale); if (!sufficientCover) { // Add the fee to First Loss Cover Pool @@ -253,15 +253,11 @@ 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, - originalPrincipalRequested); + asset, paymentParts->valueChange, managementFeeRate, loanScale); // debtDecrease may be negative, increasing the debt auto const debtDecrease = totalPaidToVault - vaultValueChange; XRPL_ASSERT_PARTS( - roundToAsset(asset, debtDecrease, originalPrincipalRequested) == - debtDecrease, + roundToAsset(asset, debtDecrease, loanScale) == debtDecrease, "ripple::LoanPay::doApply", "debtDecrease rounding good"); if (debtDecrease >= debtTotalField) diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 239eddb526..71619c4157 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -246,48 +246,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) return ret; } - auto const principalRequested = tx[sfPrincipalRequested]; - if (auto const assetsAvailable = vault->at(sfAssetsAvailable); - assetsAvailable < principalRequested) - { - JLOG(ctx.j.warn()) - << "Insufficient assets available in the Vault to fund the loan."; - return tecINSUFFICIENT_FUNDS; - } - - TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; - auto const paymentInterval = - tx[~sfPaymentInterval].value_or(defaultPaymentInterval); - auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - - auto const totalInterest = loanInterestOutstandingMinusFee( - asset, - principalRequested, - principalRequested, - interestRate, - paymentInterval, - paymentTotal, - managementFeeRate); - - auto const newDebtTotal = - brokerSle->at(sfDebtTotal) + principalRequested + totalInterest; - if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); - debtMaximum != 0 && debtMaximum < newDebtTotal) - { - JLOG(ctx.j.warn()) - << "Loan would exceed the maximum debt limit of the LoanBroker."; - return tecLIMIT_EXCEEDED; - } - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; - if (brokerSle->at(sfCoverAvailable) < - tenthBipsOfValue(newDebtTotal, coverRateMinimum)) - { - JLOG(ctx.j.warn()) - << "Insufficient first-loss capital to cover the loan."; - return tecINSUFFICIENT_FUNDS; - } - return tesSUCCESS; } @@ -327,17 +285,89 @@ LoanSet::doApply() { return tefBAD_LEDGER; // LCOV_EXCL_LINE } - auto const principalRequested = roundToAsset( - vaultAsset, tx[sfPrincipalRequested], tx[sfPrincipalRequested]); + auto const principalRequested = [&](Number const& requested) { + return roundToAsset(vaultAsset, requested, requested.exponent()); + }(tx[sfPrincipalRequested]); + auto const loanScale = principalRequested.exponent(); + + if (auto const assetsAvailable = vaultSle->at(sfAssetsAvailable); + assetsAvailable < principalRequested) + { + JLOG(j_.warn()) + << "Insufficient assets available in the Vault to fund the loan."; + return tecINSUFFICIENT_FUNDS; + } + TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)}; - auto const originationFee = tx[~sfLoanOriginationFee]; - auto const loanAssetsAvailable = - principalRequested - originationFee.value_or(Number{}); + + auto const originationFee = roundToAsset( + vaultAsset, tx[~sfLoanOriginationFee].value_or(Number{}), loanScale); + auto const loanAssetsToBorrower = principalRequested - originationFee; + + auto const paymentInterval = + tx[~sfPaymentInterval].value_or(defaultPaymentInterval); + auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); + + auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval); + auto const periodicPayment = loanPeriodicPayment( + vaultAsset, principalRequested, periodicRate, paymentTotal, loanScale); + + auto const totalValueOutstanding = loanTotalValueOutstanding( + vaultAsset, loanScale, periodicPayment, paymentTotal); + + { + // Check that some principal is paid each period. Since the first + // payment pays the least principal, if it's good, they'll all be good. + auto const paymentParts = computePaymentParts( + vaultAsset, + loanScale, + totalValueOutstanding, + principalRequested, + periodicPayment, + tx[~sfLoanServiceFee].value_or(Number{}), + periodicRate, + paymentTotal); + + if (paymentParts.principal <= 0) + { + JLOG(j_.warn()) << "Loan is unable to pay principal."; + return tecLIMIT_EXCEEDED; + } + } + + TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; + + auto const totalInterestOwedToVault = [&]() { + auto const totalInterestOutstanding = loanTotalInterestOutstanding( + principalRequested, totalValueOutstanding); + + return loanInterestOutstandingMinusFee( + vaultAsset, totalInterestOutstanding, managementFeeRate, loanScale); + }(); + + auto const newDebtTotal = brokerSle->at(sfDebtTotal) + principalRequested + + totalInterestOwedToVault; + if (auto const debtMaximum = brokerSle->at(sfDebtMaximum); + debtMaximum != 0 && debtMaximum < newDebtTotal) + { + JLOG(j_.warn()) + << "Loan would exceed the maximum debt limit of the LoanBroker."; + return tecLIMIT_EXCEEDED; + } + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + if (brokerSle->at(sfCoverAvailable) < + tenthBipsOfValue(newDebtTotal, coverRateMinimum)) + { + JLOG(j_.warn()) << "Insufficient first-loss capital to cover the loan."; + return tecINSUFFICIENT_FUNDS; + } adjustOwnerCount(view, borrowerSle, 1, j_); - auto ownerCount = borrowerSle->at(sfOwnerCount); - if (mPriorBalance < view.fees().accountReserve(ownerCount)) - return tecINSUFFICIENT_RESERVE; + { + auto ownerCount = borrowerSle->at(sfOwnerCount); + if (mPriorBalance < view.fees().accountReserve(ownerCount)) + return tecINSUFFICIENT_RESERVE; + } // Account for the origination fee using two payments // @@ -359,13 +389,13 @@ LoanSet::doApply() view, vaultPseudo, borrower, - STAmount{vaultAsset, loanAssetsAvailable}, + STAmount{vaultAsset, loanAssetsToBorrower}, j_, WaiveTransferFee::Yes)) return ter; // 2. Transfer originationFee, if any, from vault pseudo-account to // LoanBroker owner. - if (originationFee && (*originationFee != Number{})) + if (originationFee != Number{}) { // Create the holding if it doesn't already exist (necessary for MPTs). // The owner may have deleted their MPT / line at some point. @@ -383,31 +413,20 @@ LoanSet::doApply() view, vaultPseudo, brokerOwner, - STAmount{vaultAsset, *originationFee}, + STAmount{vaultAsset, originationFee}, j_, WaiveTransferFee::Yes)) return ter; } - auto const paymentInterval = - tx[~sfPaymentInterval].value_or(defaultPaymentInterval); - auto const paymentTotal = tx[~sfPaymentTotal].value_or(defaultPaymentTotal); - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; // The portion of the loan interest that will go to the vault (total // interest minus the management fee) - auto const loanInterestToVault = loanInterestOutstandingMinusFee( - vaultAsset, - principalRequested, - principalRequested, - interestRate, - paymentInterval, - paymentTotal, - managementFeeRate); auto const startDate = view.info().closeTime.time_since_epoch().count(); - auto loanSequence = brokerSle->at(sfLoanSequence); + auto loanSequenceProxy = brokerSle->at(sfLoanSequence); // Create the loan - auto loan = std::make_shared(keylet::loan(brokerID, *loanSequence)); + auto loan = + std::make_shared(keylet::loan(brokerID, *loanSequenceProxy)); // Prevent copy/paste errors auto setLoanField = @@ -417,12 +436,11 @@ LoanSet::doApply() loan->at(field) = tx[field].value_or(defValue); }; - // Set required tx fields and pre-computed fields - loan->at(sfPrincipalRequested) = principalRequested; - loan->at(sfPrincipalOutstanding) = principalRequested; + // Set required and fixed tx fields + loan->at(sfLoanScale) = principalRequested.exponent(); loan->at(sfStartDate) = startDate; loan->at(sfPaymentInterval) = paymentInterval; - loan->at(sfLoanSequence) = loanSequence; + loan->at(sfLoanSequence) = *loanSequenceProxy; loan->at(sfLoanBrokerID) = brokerID; loan->at(sfBorrower) = borrower; // Set all other transaction fields directly from the transaction @@ -438,7 +456,9 @@ LoanSet::doApply() setLoanField(~sfCloseInterestRate); setLoanField(~sfOverpaymentInterestRate); setLoanField(~sfGracePeriod, defaultGracePeriod); - // Set dynamic fields to their initial values + // Set dynamic / computed fields to their initial values + loan->at(sfPrincipalOutstanding) = principalRequested; + loan->at(sfTotalValueOutstanding) = totalValueOutstanding; loan->at(sfPreviousPaymentDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; @@ -446,7 +466,7 @@ LoanSet::doApply() // Update the balances in the vault vaultSle->at(sfAssetsAvailable) -= principalRequested; - vaultSle->at(sfAssetsTotal) += loanInterestToVault; + vaultSle->at(sfAssetsTotal) += totalInterestOwedToVault; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanSet::doApply", @@ -454,11 +474,11 @@ LoanSet::doApply() view.update(vaultSle); // Update the balances in the loan broker - brokerSle->at(sfDebtTotal) += principalRequested + loanInterestToVault; + brokerSle->at(sfDebtTotal) += principalRequested + totalInterestOwedToVault; // The broker's owner count is solely for the number of outstanding loans, // and is distinct from the broker's pseudo-account's owner count adjustOwnerCount(view, brokerSle, 1, j_); - loanSequence += 1; + loanSequenceProxy += 1; view.update(brokerSle); // Put the loan into the pseudo-account's directory