Start converting Loans to use fixed payments and track value

- Not expected to build
This commit is contained in:
Ed Hennis
2025-10-01 20:09:48 -04:00
parent d353f4a2e6
commit 63edf035a6
13 changed files with 353 additions and 305 deletions

View File

@@ -66,15 +66,15 @@ public:
static int const cMaxOffset = 80; static int const cMaxOffset = 80;
// Maximum native value supported by the code // Maximum native value supported by the code
static std::uint64_t const cMinValue = 1000000000000000ull; static std::uint64_t const cMinValue = 1'000'000'000'000'000ull;
static std::uint64_t const cMaxValue = 9999999999999999ull; static std::uint64_t const cMaxValue = 9'999'999'999'999'999ull;
static std::uint64_t const cMaxNative = 9000000000000000000ull; static std::uint64_t const cMaxNative = 9'000'000'000'000'000'000ull;
// Max native value on network. // Max native value on network.
static std::uint64_t const cMaxNativeN = 100000000000000000ull; static std::uint64_t const cMaxNativeN = 100'000'000'000'000'000ull;
static std::uint64_t const cIssuedCurrency = 0x8000000000000000ull; static std::uint64_t const cIssuedCurrency = 0x8'000'000'000'000'000ull;
static std::uint64_t const cPositive = 0x4000000000000000ull; static std::uint64_t const cPositive = 0x4'000'000'000'000'000ull;
static std::uint64_t const cMPToken = 0x2000000000000000ull; 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 cValueMask = ~(cPositive | cMPToken);
static std::uint64_t const uRateOne; static std::uint64_t const uRateOne;
@@ -695,21 +695,22 @@ divRoundStrict(
std::uint64_t std::uint64_t
getRate(STAmount const& offerOut, STAmount const& offerIn); 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 * This is used to ensure that calculations involving IOU amounts do not collect
* dust beyond the precision of the reference value. * dust beyond the precision of the reference value.
* *
* @param value The value to be rounded * @param value The value to be rounded
* @param referenceValue A reference value to establish the precision limit of * @param scale An exponent value to establish the precision limit of
* `value`. Should be larger than `value`. * `value`. Should be larger than `value.exponent()`.
* @param rounding Optional Number rounding mode * @param rounding Optional Number rounding mode
* *
*/ */
STAmount STAmount
roundToReference( roundToScale(
STAmount const value, STAmount value,
STAmount referenceValue, std::int32_t scale,
Number::rounding_mode rounding = Number::getround()); Number::rounding_mode rounding = Number::getround());
/** Round an arbitrary precision Number to the precision of a given Asset. /** Round an arbitrary precision Number to the precision of a given Asset.
@@ -720,9 +721,8 @@ roundToReference(
* *
* @param asset The relevant asset * @param asset The relevant asset
* @param value The value to be rounded * @param value The value to be rounded
* @param referenceValue Only relevant to IOU assets. A reference value to * @param scale Only relevant to IOU assets. An exponent value to establish the
* establish the precision limit of `value`. Should be larger than * precision limit of `value`. Should be larger than `value.exponent()`.
* `value`.
* @param rounding Optional Number rounding mode * @param rounding Optional Number rounding mode
*/ */
template <AssetType A> template <AssetType A>
@@ -730,7 +730,7 @@ Number
roundToAsset( roundToAsset(
A const& asset, A const& asset,
Number const& value, Number const& value,
Number const& referenceValue, std::int32_t scale,
Number::rounding_mode rounding = Number::getround()) Number::rounding_mode rounding = Number::getround())
{ {
NumberRoundModeGuard mg(rounding); NumberRoundModeGuard mg(rounding);
@@ -739,7 +739,7 @@ roundToAsset(
return ret; return ret;
// Not that the ctor will round integral types (XRP, MPT) via canonicalize, // Not that the ctor will round integral types (XRP, MPT) via canonicalize,
// so no extra work is needed for those. // so no extra work is needed for those.
return roundToReference(ret, STAmount{asset, referenceValue}); return roundToScale(ret, scale);
} }
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------

View File

@@ -548,25 +548,29 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfLoanBrokerID, soeREQUIRED}, {sfLoanBrokerID, soeREQUIRED},
{sfLoanSequence, soeREQUIRED}, {sfLoanSequence, soeREQUIRED},
{sfBorrower, soeREQUIRED}, {sfBorrower, soeREQUIRED},
{sfLoanOriginationFee, soeREQUIRED}, {sfLoanOriginationFee, soeDEFAULT},
{sfLoanServiceFee, soeREQUIRED}, {sfLoanServiceFee, soeDEFAULT},
{sfLatePaymentFee, soeREQUIRED}, {sfLatePaymentFee, soeDEFAULT},
{sfClosePaymentFee, soeREQUIRED}, {sfClosePaymentFee, soeDEFAULT},
{sfOverpaymentFee, soeREQUIRED}, {sfOverpaymentFee, soeDEFAULT},
{sfInterestRate, soeREQUIRED}, {sfInterestRate, soeDEFAULT},
{sfLateInterestRate, soeREQUIRED}, {sfLateInterestRate, soeDEFAULT},
{sfCloseInterestRate, soeREQUIRED}, {sfCloseInterestRate, soeDEFAULT},
{sfOverpaymentInterestRate, soeREQUIRED}, {sfOverpaymentInterestRate, soeDEFAULT},
{sfStartDate, soeREQUIRED}, {sfStartDate, soeREQUIRED},
{sfPaymentInterval, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED},
{sfGracePeriod, soeREQUIRED}, {sfGracePeriod, soeREQUIRED},
{sfPeriodicPayment, soeREQUIRED},
{sfPreviousPaymentDate, soeREQUIRED}, {sfPreviousPaymentDate, soeREQUIRED},
{sfNextPaymentDueDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED},
{sfPaymentRemaining, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED},
{sfPrincipalOutstanding, soeREQUIRED}, {sfPrincipalOutstanding, soeREQUIRED},
// Save the original request amount for rounding / scaling of {sfTotalValueOutstanding, soeDEFAULT},
// other computations, particularly for IOUs // Based on the original principal borrowed, used for
{sfPrincipalRequested, soeREQUIRED}, // 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 #undef EXPAND

View File

@@ -239,12 +239,11 @@ TYPED_SFIELD(sfLatePaymentFee, NUMBER, 11)
TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12) TYPED_SFIELD(sfClosePaymentFee, NUMBER, 12)
TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13) TYPED_SFIELD(sfPrincipalOutstanding, NUMBER, 13)
TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14) TYPED_SFIELD(sfPrincipalRequested, NUMBER, 14)
TYPED_SFIELD(sfTotalValueOutstanding, NUMBER, 15)
TYPED_SFIELD(sfPeriodicPayment, NUMBER, 16)
// int32 // int32
// NOTE: Do not use `sfDummyInt32`. It's so far the only use of INT32 TYPED_SFIELD(sfLoanScale, INT32, 1)
// 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
// currency amount (common) // currency amount (common)
TYPED_SFIELD(sfAmount, AMOUNT, 1) TYPED_SFIELD(sfAmount, AMOUNT, 1)

View File

@@ -1510,32 +1510,28 @@ canonicalizeRoundStrict(
} }
STAmount STAmount
roundToReference( roundToScale(STAmount value, std::int32_t scale, Number::rounding_mode rounding)
STAmount const value,
STAmount referenceValue,
Number::rounding_mode rounding)
{ {
// Nothing to do for intgral types. // Nothing to do for intgral types.
if (value.asset().native() || !value.asset().holds<Issue>()) if (value.asset().native() || !value.asset().holds<Issue>())
return value; return value;
// If the value is greater than or equal to the referenceValue (ignoring // If the value's exponent is greater than or equal to the scale, then
// sign), then rounding will do nothing, so just return the value. // rounding will do nothing, and might even lose precision, so just return
if (value.exponent() > referenceValue.exponent() || // the value.
(value.exponent() == referenceValue.exponent() && if (value.exponent() >= scale)
value.mantissa() >= referenceValue.mantissa()))
return value; return value;
if (referenceValue.negative() != value.negative()) STAmount referenceValue{
referenceValue.negate(); value.asset(), STAmount::cMinValue, scale, value.negative()};
NumberRoundModeGuard mg(rounding); NumberRoundModeGuard mg(rounding);
// With an IOU, the total will be truncated to the precision of the // With an IOU, the total will be truncated to the precision of the
// larger value: referenceValue // larger value: referenceValue
STAmount const total = referenceValue + value; value += referenceValue;
// Remove the reference value, and we're left with the rounded value. // Remove the reference value, and we're left with the rounded value.
STAmount const result = total - referenceValue; value -= referenceValue;
return result; return value;
} }
namespace { namespace {

View File

@@ -111,7 +111,7 @@ class Loan_test : public beast::unit_test::suite
NetClock::time_point startDate = {}; NetClock::time_point startDate = {};
std::uint32_t nextPaymentDate = 0; std::uint32_t nextPaymentDate = 0;
std::uint32_t paymentRemaining = 0; std::uint32_t paymentRemaining = 0;
Number const principalRequested = 0; std::int32_t const loanScale = 0;
Number principalOutstanding = 0; Number principalOutstanding = 0;
std::uint32_t flags = 0; std::uint32_t flags = 0;
std::uint32_t paymentInterval = 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 nextPaymentDate,
std::uint32_t paymentRemaining, std::uint32_t paymentRemaining,
Number const& principalRequested, Number const& principalRequested,
Number const& principalOutstanding, Number const& loanScale,
std::uint32_t flags) const std::uint32_t flags) const
{ {
using namespace jtx; using namespace jtx;
@@ -233,13 +233,12 @@ class Loan_test : public beast::unit_test::suite
loan->at(sfNextPaymentDueDate) == nextPaymentDate); loan->at(sfNextPaymentDueDate) == nextPaymentDate);
env.test.BEAST_EXPECT( env.test.BEAST_EXPECT(
loan->at(sfPaymentRemaining) == paymentRemaining); loan->at(sfPaymentRemaining) == paymentRemaining);
env.test.BEAST_EXPECT( env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale);
loan->at(sfPrincipalRequested) == principalRequested);
env.test.BEAST_EXPECT( env.test.BEAST_EXPECT(
loan->at(sfPrincipalOutstanding) == principalOutstanding); loan->at(sfPrincipalOutstanding) == principalOutstanding);
env.test.BEAST_EXPECT( env.test.BEAST_EXPECT(
loan->at(sfPrincipalRequested) == loan->at(sfLoanScale) ==
broker.asset(loanAmount).value()); broker.asset(loanAmount).value().exponent());
env.test.BEAST_EXPECT(loan->at(sfFlags) == flags); env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; 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)}}, .startDate = tp{d{loan->at(sfStartDate)}},
.nextPaymentDate = loan->at(sfNextPaymentDueDate), .nextPaymentDate = loan->at(sfNextPaymentDueDate),
.paymentRemaining = loan->at(sfPaymentRemaining), .paymentRemaining = loan->at(sfPaymentRemaining),
.principalRequested = loan->at(sfPrincipalRequested), .loanScale = loan->at(sfLoanScale),
.principalOutstanding = loan->at(sfPrincipalOutstanding), .principalOutstanding = loan->at(sfPrincipalOutstanding),
.flags = loan->at(sfFlags), .flags = loan->at(sfFlags),
.paymentInterval = loan->at(sfPaymentInterval), .paymentInterval = loan->at(sfPaymentInterval),
@@ -611,7 +610,7 @@ class Loan_test : public beast::unit_test::suite
BEAST_EXPECT( BEAST_EXPECT(
loan->at(sfNextPaymentDueDate) == startDate + interval); loan->at(sfNextPaymentDueDate) == startDate + interval);
BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); 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); 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[sfPaymentRemaining] == 1);
BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0); BEAST_EXPECT(loan[sfPreviousPaymentDate] == 0);
BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000"); BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000");
BEAST_EXPECT(loan[sfPrincipalRequested] == "1000000000"); BEAST_EXPECT(loan[sfLoanScale] == 0);
BEAST_EXPECT( BEAST_EXPECT(
loan[sfStartDate].asUInt() == loan[sfStartDate].asUInt() ==
startDate.time_since_epoch().count()); startDate.time_since_epoch().count());
@@ -2183,7 +2182,7 @@ class Loan_test : public beast::unit_test::suite
{ {
// Verify the payment decreased the principal // Verify the payment decreased the principal
BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); 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); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal);
} }
@@ -2196,7 +2195,7 @@ class Loan_test : public beast::unit_test::suite
{ {
// Verify the payment decreased the principal // Verify the payment decreased the principal
BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments - 1); BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments - 1);
BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT(loan->at(sfLoanScale) == actualPrincipal.exponent());
BEAST_EXPECT( BEAST_EXPECT(
loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); loan->at(sfPrincipalOutstanding) == actualPrincipal - 1);
} }

View File

@@ -743,63 +743,63 @@ class STParsedJSON_test : public beast::unit_test::suite
{ {
Json::Value j; Json::Value j;
int const minInt32 = -2147483648; int const minInt32 = -2147483648;
j[sfDummyInt32] = minInt32; j[sfLoanScale] = minInt32;
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value()); 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) == minInt32); BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == minInt32);
} }
// max value // max value
{ {
Json::Value j; Json::Value j;
int const maxInt32 = 2147483647; int const maxInt32 = 2147483647;
j[sfDummyInt32] = maxInt32; j[sfLoanScale] = maxInt32;
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value()); 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) == maxInt32); BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == maxInt32);
} }
// max uint value // max uint value
{ {
Json::Value j; Json::Value j;
unsigned int const maxUInt32 = 2147483647u; unsigned int const maxUInt32 = 2147483647u;
j[sfDummyInt32] = maxUInt32; j[sfLoanScale] = maxUInt32;
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value()); BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT( BEAST_EXPECT(
obj.object->getFieldI32(sfDummyInt32) == obj.object->getFieldI32(sfLoanScale) ==
static_cast<int32_t>(maxUInt32)); static_cast<int32_t>(maxUInt32));
} }
// Test with string value // Test with string value
{ {
Json::Value j; Json::Value j;
j[sfDummyInt32] = "2147483647"; j[sfLoanScale] = "2147483647";
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value()); BEAST_EXPECT(obj.object.has_value());
if (BEAST_EXPECT(obj.object->isFieldPresent(sfDummyInt32))) if (BEAST_EXPECT(obj.object->isFieldPresent(sfLoanScale)))
BEAST_EXPECT( BEAST_EXPECT(
obj.object->getFieldI32(sfDummyInt32) == 2147483647u); obj.object->getFieldI32(sfLoanScale) == 2147483647u);
} }
// Test with string negative value // Test with string negative value
{ {
Json::Value j; Json::Value j;
int value = -2147483648; int value = -2147483648;
j[sfDummyInt32] = std::to_string(value); j[sfLoanScale] = std::to_string(value);
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(obj.object.has_value()); 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) == value); BEAST_EXPECT(obj.object->getFieldI32(sfLoanScale) == value);
} }
// Test out of range value for int32 (negative) // Test out of range value for int32 (negative)
{ {
Json::Value j; Json::Value j;
j[sfDummyInt32] = "-2147483649"; j[sfLoanScale] = "-2147483649";
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value()); 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) // Test out of range value for int32 (positive)
{ {
Json::Value j; Json::Value j;
j[sfDummyInt32] = 2147483648u; j[sfLoanScale] = 2147483648u;
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value()); BEAST_EXPECT(!obj.object.has_value());
} }
@@ -815,7 +815,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test string value out of range // Test string value out of range
{ {
Json::Value j; Json::Value j;
j[sfDummyInt32] = "2147483648"; j[sfLoanScale] = "2147483648";
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value()); BEAST_EXPECT(!obj.object.has_value());
} }
@@ -823,7 +823,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test bad_type (arrayValue) // Test bad_type (arrayValue)
{ {
Json::Value j; Json::Value j;
j[sfDummyInt32] = Json::Value(Json::arrayValue); j[sfLoanScale] = Json::Value(Json::arrayValue);
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value()); BEAST_EXPECT(!obj.object.has_value());
} }
@@ -831,7 +831,7 @@ class STParsedJSON_test : public beast::unit_test::suite
// Test bad_type (objectValue) // Test bad_type (objectValue)
{ {
Json::Value j; Json::Value j;
j[sfDummyInt32] = Json::Value(Json::objectValue); j[sfLoanScale] = Json::Value(Json::objectValue);
STParsedJSONObject obj("Test", j); STParsedJSONObject obj("Test", j);
BEAST_EXPECT(!obj.object.has_value()); BEAST_EXPECT(!obj.object.has_value());
} }

View File

@@ -40,22 +40,100 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval);
Number Number
loanPeriodicPayment( loanPeriodicPayment(
Number principalOutstanding, Number const& principalOutstanding,
Number periodicRate, Number const& periodicRate,
std::uint32_t paymentsRemaining); std::uint32_t paymentsRemaining);
Number Number
loanPeriodicPayment( loanPeriodicPayment(
Number principalOutstanding, Number const& principalOutstanding,
TenthBips32 interestRate, TenthBips32 interestRate,
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining); 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 <AssetType A>
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 <AssetType A>
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 <AssetType A>
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 <AssetType A> template <AssetType A>
Number Number
loanTotalValueOutstanding( loanTotalValueOutstanding(
A asset, A asset,
Number const& originalPrincipal, std::int32_t scale,
Number const& periodicPayment, Number const& periodicPayment,
std::uint32_t paymentsRemaining) std::uint32_t paymentsRemaining)
{ {
@@ -66,7 +144,7 @@ loanTotalValueOutstanding(
* Value Calculation), specifically "totalValueOutstanding = ..." * Value Calculation), specifically "totalValueOutstanding = ..."
*/ */
periodicPayment * paymentsRemaining, periodicPayment * paymentsRemaining,
originalPrincipal, scale,
Number::upward); Number::upward);
} }
@@ -74,7 +152,7 @@ template <AssetType A>
Number Number
loanTotalValueOutstanding( loanTotalValueOutstanding(
A asset, A asset,
Number const& originalPrincipal, std::int32_t scale,
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 interestRate, TenthBips32 interestRate,
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
@@ -86,19 +164,21 @@ loanTotalValueOutstanding(
*/ */
return loanTotalValueOutstanding( return loanTotalValueOutstanding(
asset, asset,
originalPrincipal, scale,
loanPeriodicPayment( loanPeriodicPayment(
asset,
principalOutstanding, principalOutstanding,
interestRate, interestRate,
paymentInterval, paymentInterval,
paymentsRemaining), paymentsRemaining,
scale),
paymentsRemaining); paymentsRemaining);
} }
inline Number inline Number
loanTotalInterestOutstanding( loanTotalInterestOutstanding(
Number principalOutstanding, Number const& principalOutstanding,
Number totalValueOutstanding) Number const& totalValueOutstanding)
{ {
/* /*
* This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan * This formula is from the XLS-66 spec, section 3.2.4.2 (Total Loan
@@ -111,7 +191,7 @@ template <AssetType A>
Number Number
loanTotalInterestOutstanding( loanTotalInterestOutstanding(
A asset, A asset,
Number const& originalPrincipal, std::int32_t scale,
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 interestRate, TenthBips32 interestRate,
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
@@ -125,94 +205,47 @@ loanTotalInterestOutstanding(
principalOutstanding, principalOutstanding,
loanTotalValueOutstanding( loanTotalValueOutstanding(
asset, asset,
originalPrincipal, scale,
principalOutstanding, principalOutstanding,
interestRate, interestRate,
paymentInterval, paymentInterval,
paymentsRemaining)); 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 <AssetType A> template <AssetType A>
Number Number
valueMinusManagementFee( loanInterestOutstandingMinusFee(
A const& asset, A const& asset,
Number const& value, Number const& totalInterestOutstanding,
TenthBips32 managementFeeRate, TenthBips32 managementFeeRate,
Number const& originalPrincipal) std::int32_t scale)
{ {
return roundToAsset( return valueMinusManagementFee(
asset, asset, totalInterestOutstanding, managementFeeRate, scale);
detail::minusManagementFee(value, managementFeeRate),
originalPrincipal);
} }
template <AssetType A> template <AssetType A>
Number Number
loanInterestOutstandingMinusFee( loanInterestOutstandingMinusFee(
A const& asset, A const& asset,
Number const& originalPrincipal, std::int32_t scale,
Number const& principalOutstanding, Number const& principalOutstanding,
TenthBips32 interestRate, TenthBips32 interestRate,
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining, std::uint32_t paymentsRemaining,
TenthBips32 managementFeeRate) TenthBips32 managementFeeRate)
{ {
return valueMinusManagementFee( return loanInterestOutstandingMinusFee(
asset, asset,
detail::loanTotalInterestOutstanding( loanTotalInterestOutstanding(
asset, asset,
originalPrincipal, scale,
principalOutstanding, principalOutstanding,
interestRate, interestRate,
paymentInterval, paymentInterval,
paymentsRemaining), paymentsRemaining),
managementFeeRate, managementFeeRate,
originalPrincipal); scale);
}
template <AssetType A>
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);
} }
template <AssetType A> template <AssetType A>
@@ -224,7 +257,7 @@ loanLatePaymentInterest(
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t startDate, std::uint32_t startDate,
std::uint32_t prevPaymentDate, std::uint32_t prevPaymentDate,
Number const& originalPrincipal) Number const& scale)
{ {
return roundToAsset( return roundToAsset(
asset, asset,
@@ -234,7 +267,15 @@ loanLatePaymentInterest(
parentCloseTime, parentCloseTime,
startDate, startDate,
prevPaymentDate), prevPaymentDate),
originalPrincipal); scale);
}
template <AssetType A>
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 struct PaymentParts
@@ -246,9 +287,10 @@ struct PaymentParts
template <AssetType A> template <AssetType A>
PaymentParts PaymentParts
computePeriodicPaymentParts( computePaymentParts(
A const& asset, A const& asset,
Number const& originalPrincipal, std::int32_t scale,
Number const& totalValueOutstanding,
Number const& principalOutstanding, Number const& principalOutstanding,
Number const& periodicPaymentAmount, Number const& periodicPaymentAmount,
Number const& serviceFee, Number const& serviceFee,
@@ -259,16 +301,17 @@ computePeriodicPaymentParts(
* This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular * This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular
* Payment) * Payment)
*/ */
Number const roundedFee = XRPL_ASSERT_PARTS(
roundToAsset(asset, serviceFee, originalPrincipal); rounded(asset, totalValueOutstanding, scale) &&
if (paymentRemaining == 1) 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. // If there's only one payment left, we need to pay off the principal.
Number const interest = roundToAsset( Number const interest = totalValueOutstanding - principalOutstanding;
asset,
periodicPaymentAmount - principalOutstanding,
originalPrincipal,
Number::upward);
return { return {
.interest = interest, .interest = interest,
.principal = principalOutstanding, .principal = principalOutstanding,
@@ -284,10 +327,7 @@ computePeriodicPaymentParts(
* Because those values deal with funds, they need to be rounded. * Because those values deal with funds, they need to be rounded.
*/ */
Number const interest = roundToAsset( Number const interest = roundToAsset(
asset, asset, principalOutstanding * periodicRate, scale, Number::upward);
principalOutstanding * periodicRate,
originalPrincipal,
Number::upward);
XRPL_ASSERT( XRPL_ASSERT(
interest >= 0, interest >= 0,
"ripple::detail::computePeriodicPayment : valid interest"); "ripple::detail::computePeriodicPayment : valid interest");
@@ -296,8 +336,8 @@ computePeriodicPaymentParts(
// payment amount, ensuring that some principal is paid regardless of any // payment amount, ensuring that some principal is paid regardless of any
// other results. // other results.
auto const roundedPayment = [&]() { auto const roundedPayment = [&]() {
auto roundedPayment = roundToAsset( auto roundedPayment =
asset, periodicPaymentAmount, originalPrincipal, Number::upward); roundToAsset(asset, periodicPaymentAmount, scale, Number::upward);
if (roundedPayment > interest) if (roundedPayment > interest)
return roundedPayment; return roundedPayment;
auto newPayment = roundedPayment; auto newPayment = roundedPayment;
@@ -310,21 +350,21 @@ computePeriodicPaymentParts(
{ {
// Non-integral types: IOU. Add "dust" that will not be lost in // Non-integral types: IOU. Add "dust" that will not be lost in
// rounding. // rounding.
auto const epsilon = Number{1, originalPrincipal.exponent() - 14}; auto const epsilon = Number{1, scale - 14};
newPayment += epsilon; newPayment += epsilon;
} }
roundedPayment = roundToAsset(asset, newPayment, originalPrincipal); roundedPayment = roundToAsset(asset, newPayment, scale);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
roundedPayment == newPayment, roundedPayment == newPayment,
"ripple::computePeriodicPaymentParts", "ripple::computePaymentParts",
"epsilon preserved in rounding"); "epsilon preserved in rounding");
return roundedPayment; return roundedPayment;
}(); }();
Number const principal = Number const principal =
roundToAsset(asset, roundedPayment - interest, originalPrincipal); roundToAsset(asset, roundedPayment - interest, scale);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
principal > 0 && principal <= principalOutstanding, principal > 0 && principal <= principalOutstanding,
"ripple::computePeriodicPaymentParts", "ripple::computePaymentParts",
"valid principal"); "valid principal");
return {.interest = interest, .principal = principal, .fee = roundedFee}; return {.interest = interest, .principal = principal, .fee = roundedFee};
@@ -375,7 +415,7 @@ handleLatePayment(
std::uint32_t const startDate, std::uint32_t const startDate,
std::uint32_t const paymentInterval, std::uint32_t const paymentInterval,
TenthBips32 const lateInterestRate, TenthBips32 const lateInterestRate,
Number const& originalPrincipalRequested, std::int32_t loanScale,
Number const& latePaymentFee, Number const& latePaymentFee,
STAmount const& amount, STAmount const& amount,
beast::Journal j) beast::Journal j)
@@ -393,7 +433,7 @@ handleLatePayment(
view.parentCloseTime(), view.parentCloseTime(),
startDate, startDate,
prevPaymentDateProxy, prevPaymentDateProxy,
originalPrincipalRequested); loanScale);
XRPL_ASSERT( XRPL_ASSERT(
latePaymentInterest >= 0, latePaymentInterest >= 0,
"ripple::handleLatePayment : valid late interest"); "ripple::handleLatePayment : valid late interest");
@@ -446,7 +486,7 @@ handleFullPayment(
std::uint32_t const startDate, std::uint32_t const startDate,
std::uint32_t const paymentInterval, std::uint32_t const paymentInterval,
TenthBips32 const closeInterestRate, TenthBips32 const closeInterestRate,
Number const& originalPrincipalRequested, std::int32_t loanScale,
Number const& totalInterestOutstanding, Number const& totalInterestOutstanding,
Number const& periodicRate, Number const& periodicRate,
Number const& closePaymentFee, Number const& closePaymentFee,
@@ -468,14 +508,14 @@ handleFullPayment(
startDate, startDate,
prevPaymentDateProxy, prevPaymentDateProxy,
paymentInterval), paymentInterval),
originalPrincipalRequested); loanScale);
XRPL_ASSERT( XRPL_ASSERT(
accruedInterest >= 0, accruedInterest >= 0,
"ripple::handleFullPayment : valid accrued interest"); "ripple::handleFullPayment : valid accrued interest");
auto const prepaymentPenalty = roundToAsset( auto const prepaymentPenalty = roundToAsset(
asset, asset,
tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate), tenthBipsOfValue(principalOutstandingProxy.value(), closeInterestRate),
originalPrincipalRequested); loanScale);
XRPL_ASSERT( XRPL_ASSERT(
prepaymentPenalty >= 0, prepaymentPenalty >= 0,
"ripple::handleFullPayment : valid prepayment " "ripple::handleFullPayment : valid prepayment "
@@ -521,7 +561,8 @@ loanMakePayment(
* This function is an implementation of the XLS-66 spec, * This function is an implementation of the XLS-66 spec,
* section 3.2.4.3 (Transaction Pseudo-code) * 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); auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment);
@@ -533,8 +574,8 @@ loanMakePayment(
Number const serviceFee = loan->at(sfLoanServiceFee); Number const serviceFee = loan->at(sfLoanServiceFee);
Number const latePaymentFee = loan->at(sfLatePaymentFee); Number const latePaymentFee = loan->at(sfLatePaymentFee);
Number const closePaymentFee = roundToAsset( Number const closePaymentFee =
asset, loan->at(sfClosePaymentFee), originalPrincipalRequested); roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)};
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
@@ -567,26 +608,23 @@ loanMakePayment(
periodicPaymentAmount > 0, periodicPaymentAmount > 0,
"ripple::computePeriodicPayment : valid payment"); "ripple::computePeriodicPayment : valid payment");
auto const periodic = computePeriodicPaymentParts( auto const periodic = computePaymentParts(
asset, asset,
originalPrincipalRequested, loanScale,
totalValueOutstandingProxy,
principalOutstandingProxy, principalOutstandingProxy,
periodicPaymentAmount, periodicPaymentAmount,
serviceFee, serviceFee,
periodicRate, periodicRate,
paymentRemainingProxy); paymentRemainingProxy);
Number const totalValueOutstanding = detail::loanTotalValueOutstanding( Number const totalValueOutstanding = loanTotalValueOutstanding(
asset, asset, loanScale, periodicPaymentAmount, paymentRemainingProxy);
originalPrincipalRequested,
periodicPaymentAmount,
paymentRemainingProxy);
XRPL_ASSERT( XRPL_ASSERT(
totalValueOutstanding > 0, totalValueOutstanding > 0,
"ripple::loanMakePayment : valid total value"); "ripple::loanMakePayment : valid total value");
Number const totalInterestOutstanding = Number const totalInterestOutstanding = loanTotalInterestOutstanding(
detail::loanTotalInterestOutstanding( principalOutstandingProxy, totalValueOutstanding);
principalOutstandingProxy, totalValueOutstanding);
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
totalInterestOutstanding >= 0, totalInterestOutstanding >= 0,
"ripple::loanMakePayment", "ripple::loanMakePayment",
@@ -612,7 +650,7 @@ loanMakePayment(
startDate, startDate,
paymentInterval, paymentInterval,
lateInterestRate, lateInterestRate,
originalPrincipalRequested, loanScale,
latePaymentFee, latePaymentFee,
amount, amount,
j)) j))
@@ -631,7 +669,7 @@ loanMakePayment(
startDate, startDate,
paymentInterval, paymentInterval,
closeInterestRate, closeInterestRate,
originalPrincipalRequested, loanScale,
totalInterestOutstanding, totalInterestOutstanding,
periodicRate, periodicRate,
closePaymentFee, closePaymentFee,
@@ -682,9 +720,10 @@ loanMakePayment(
{ {
// Only do the work if we need to // Only do the work if we need to
if (!future) if (!future)
future = computePeriodicPaymentParts( future = computePaymentParts(
asset, asset,
originalPrincipalRequested, loanScale,
totalValueOutstandingProxy,
principalOutstandingProxy, principalOutstandingProxy,
periodicPaymentAmount, periodicPaymentAmount,
serviceFee, serviceFee,
@@ -707,9 +746,9 @@ loanMakePayment(
Number totalFeePaid = serviceFee * fullPeriodicPayments; Number totalFeePaid = serviceFee * fullPeriodicPayments;
Number const newInterest = detail::loanTotalInterestOutstanding( Number const newInterest = loanTotalInterestOutstanding(
asset, asset,
originalPrincipalRequested, loanScale,
principalOutstandingProxy, principalOutstandingProxy,
interestRate, interestRate,
paymentInterval, paymentInterval,
@@ -725,20 +764,18 @@ loanMakePayment(
principalOutstandingProxy.value(), principalOutstandingProxy.value(),
amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid));
if (roundToAsset(asset, overpayment, originalPrincipalRequested) > 0) if (roundToAsset(asset, overpayment, loanScale) > 0)
{ {
Number const interestPortion = roundToAsset( Number const interestPortion = roundToAsset(
asset, asset,
tenthBipsOfValue(overpayment, overpaymentInterestRate), tenthBipsOfValue(overpayment, overpaymentInterestRate),
originalPrincipalRequested); loanScale);
Number const feePortion = roundToAsset( Number const feePortion = roundToAsset(
asset, asset,
tenthBipsOfValue(overpayment, overpaymentFee), tenthBipsOfValue(overpayment, overpaymentFee),
originalPrincipalRequested); loanScale);
Number const remainder = roundToAsset( Number const remainder = roundToAsset(
asset, asset, overpayment - interestPortion - feePortion, loanScale);
overpayment - interestPortion - feePortion,
originalPrincipalRequested);
// Don't process an overpayment if the whole amount (or more!) // Don't process an overpayment if the whole amount (or more!)
// gets eaten by fees // gets eaten by fees
@@ -760,20 +797,17 @@ loanMakePayment(
// Check the final results are rounded, to double-check that the // Check the final results are rounded, to double-check that the
// intermediate steps were rounded. // intermediate steps were rounded.
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalPrincipalPaid, originalPrincipalRequested) == roundToAsset(asset, totalPrincipalPaid, loanScale) ==
totalPrincipalPaid, totalPrincipalPaid,
"ripple::loanMakePayment : totalPrincipalPaid rounded"); "ripple::loanMakePayment : totalPrincipalPaid rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalInterestPaid, originalPrincipalRequested) == roundToAsset(asset, totalInterestPaid, loanScale) == totalInterestPaid,
totalInterestPaid,
"ripple::loanMakePayment : totalInterestPaid rounded"); "ripple::loanMakePayment : totalInterestPaid rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, loanValueChange, originalPrincipalRequested) == roundToAsset(asset, loanValueChange, loanScale) == loanValueChange,
loanValueChange,
"ripple::loanMakePayment : loanValueChange rounded"); "ripple::loanMakePayment : loanValueChange rounded");
XRPL_ASSERT( XRPL_ASSERT(
roundToAsset(asset, totalFeePaid, originalPrincipalRequested) == roundToAsset(asset, totalFeePaid, loanScale) == totalFeePaid,
totalFeePaid,
"ripple::loanMakePayment : totalFeePaid rounded"); "ripple::loanMakePayment : totalFeePaid rounded");
return LoanPaymentParts{ return LoanPaymentParts{
.principalPaid = totalPrincipalPaid, .principalPaid = totalPrincipalPaid,

View File

@@ -48,8 +48,8 @@ loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
Number Number
loanPeriodicPayment( loanPeriodicPayment(
Number principalOutstanding, Number const& principalOutstanding,
Number periodicRate, Number const& periodicRate,
std::uint32_t paymentsRemaining) std::uint32_t paymentsRemaining)
{ {
if (principalOutstanding == 0 || paymentsRemaining == 0) if (principalOutstanding == 0 || paymentsRemaining == 0)
@@ -72,7 +72,7 @@ loanPeriodicPayment(
Number Number
loanPeriodicPayment( loanPeriodicPayment(
Number principalOutstanding, Number const& principalOutstanding,
TenthBips32 interestRate, TenthBips32 interestRate,
std::uint32_t paymentInterval, std::uint32_t paymentInterval,
std::uint32_t paymentsRemaining) std::uint32_t paymentsRemaining)
@@ -91,7 +91,7 @@ loanPeriodicPayment(
Number Number
loanLatePaymentInterest( loanLatePaymentInterest(
Number principalOutstanding, Number const& principalOutstanding,
TenthBips32 lateInterestRate, TenthBips32 lateInterestRate,
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t startDate, std::uint32_t startDate,
@@ -114,8 +114,8 @@ loanLatePaymentInterest(
Number Number
loanAccruedInterest( loanAccruedInterest(
Number principalOutstanding, Number const& principalOutstanding,
Number periodicRate, Number const& periodicRate,
NetClock::time_point parentCloseTime, NetClock::time_point parentCloseTime,
std::uint32_t startDate, std::uint32_t startDate,
std::uint32_t prevPaymentDate, std::uint32_t prevPaymentDate,

View File

@@ -2203,7 +2203,7 @@ NoModifiedUnmodifiableFields::finalize(
fieldChanged(before, after, sfStartDate) || fieldChanged(before, after, sfStartDate) ||
fieldChanged(before, after, sfPaymentInterval) || fieldChanged(before, after, sfPaymentInterval) ||
fieldChanged(before, after, sfGracePeriod) || fieldChanged(before, after, sfGracePeriod) ||
fieldChanged(before, after, sfPrincipalRequested); fieldChanged(before, after, sfLoanScale);
break; break;
default: default:
/* /*

View File

@@ -146,7 +146,7 @@ LoanBrokerCoverWithdraw::preclaim(PreclaimContext const& ctx)
vaultAsset, vaultAsset,
tenthBipsOfValue( tenthBipsOfValue(
currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))), currentDebtTotal, TenthBips32(sleBroker->at(sfCoverRateMinimum))),
currentDebtTotal); currentDebtTotal.exponent());
if (coverAvail < amount) if (coverAvail < amount)
return tecINSUFFICIENT_FUNDS; return tecINSUFFICIENT_FUNDS;
if ((coverAvail - amount) < minimumCover) if ((coverAvail - amount) < minimumCover)

View File

@@ -145,7 +145,7 @@ LoanManage::defaultLoan(
{ {
// Calculate the amount of the Default that First-Loss Capital covers: // 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)}; TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal);
auto const totalDefaultAmount = principalOutstanding + interestOutstanding; auto const totalDefaultAmount = principalOutstanding + interestOutstanding;
@@ -162,7 +162,7 @@ LoanManage::defaultLoan(
brokerDebtTotalProxy.value(), coverRateMinimum), brokerDebtTotalProxy.value(), coverRateMinimum),
coverRateLiquidation), coverRateLiquidation),
totalDefaultAmount), totalDefaultAmount),
originalPrincipalRequested); loanScale);
auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered; auto const vaultDefaultAmount = totalDefaultAmount - defaultCovered;
@@ -380,7 +380,7 @@ LoanManage::doApply()
auto const vaultAsset = vaultSle->at(sfAsset); auto const vaultAsset = vaultSle->at(sfAsset);
TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; 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); auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
@@ -388,7 +388,7 @@ LoanManage::doApply()
auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); auto const paymentsRemaining = loanSle->at(sfPaymentRemaining);
auto const interestOutstanding = loanInterestOutstandingMinusFee( auto const interestOutstanding = loanInterestOutstandingMinusFee(
vaultAsset, vaultAsset,
originalPrincipalRequested, loanScale,
principalOutstanding.value(), principalOutstanding.value(),
interestRate, interestRate,
paymentInterval, paymentInterval,

View File

@@ -147,7 +147,7 @@ LoanPay::doApply()
//------------------------------------------------------ //------------------------------------------------------
// Loan object state changes // 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 // 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 // attempted, so the original values can be used. If the payment fails, this
@@ -163,7 +163,7 @@ LoanPay::doApply()
auto const interestOutstanding = loanInterestOutstandingMinusFee( auto const interestOutstanding = loanInterestOutstandingMinusFee(
asset, asset,
originalPrincipalRequested, loanScale,
principalOutstanding.value(), principalOutstanding.value(),
interestRate, interestRate,
paymentInterval, paymentInterval,
@@ -219,7 +219,7 @@ LoanPay::doApply()
auto const managementFee = roundToAsset( auto const managementFee = roundToAsset(
asset, asset,
tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate),
originalPrincipalRequested); loanScale);
auto const totalPaidToVault = paymentParts->principalPaid + auto const totalPaidToVault = paymentParts->principalPaid +
paymentParts->interestPaid - managementFee; paymentParts->interestPaid - managementFee;
@@ -241,7 +241,7 @@ LoanPay::doApply()
bool const sufficientCover = coverAvailableField >= bool const sufficientCover = coverAvailableField >=
roundToAsset(asset, roundToAsset(asset,
tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), tenthBipsOfValue(debtTotalField.value(), coverRateMinimum),
originalPrincipalRequested); loanScale);
if (!sufficientCover) if (!sufficientCover)
{ {
// Add the fee to First Loss Cover Pool // 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, // Decrease LoanBroker Debt by the amount paid, add the Loan value change,
// and subtract the change in the management fee // and subtract the change in the management fee
auto const vaultValueChange = valueMinusManagementFee( auto const vaultValueChange = valueMinusManagementFee(
asset, asset, paymentParts->valueChange, managementFeeRate, loanScale);
paymentParts->valueChange,
managementFeeRate,
originalPrincipalRequested);
// debtDecrease may be negative, increasing the debt // debtDecrease may be negative, increasing the debt
auto const debtDecrease = totalPaidToVault - vaultValueChange; auto const debtDecrease = totalPaidToVault - vaultValueChange;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
roundToAsset(asset, debtDecrease, originalPrincipalRequested) == roundToAsset(asset, debtDecrease, loanScale) == debtDecrease,
debtDecrease,
"ripple::LoanPay::doApply", "ripple::LoanPay::doApply",
"debtDecrease rounding good"); "debtDecrease rounding good");
if (debtDecrease >= debtTotalField) if (debtDecrease >= debtTotalField)

View File

@@ -246,48 +246,6 @@ LoanSet::preclaim(PreclaimContext const& ctx)
return ret; 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; return tesSUCCESS;
} }
@@ -327,17 +285,89 @@ LoanSet::doApply()
{ {
return tefBAD_LEDGER; // LCOV_EXCL_LINE return tefBAD_LEDGER; // LCOV_EXCL_LINE
} }
auto const principalRequested = roundToAsset( auto const principalRequested = [&](Number const& requested) {
vaultAsset, tx[sfPrincipalRequested], tx[sfPrincipalRequested]); 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)}; TenthBips32 const interestRate{tx[~sfInterestRate].value_or(0)};
auto const originationFee = tx[~sfLoanOriginationFee];
auto const loanAssetsAvailable = auto const originationFee = roundToAsset(
principalRequested - originationFee.value_or(Number{}); 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_); adjustOwnerCount(view, borrowerSle, 1, j_);
auto ownerCount = borrowerSle->at(sfOwnerCount); {
if (mPriorBalance < view.fees().accountReserve(ownerCount)) auto ownerCount = borrowerSle->at(sfOwnerCount);
return tecINSUFFICIENT_RESERVE; if (mPriorBalance < view.fees().accountReserve(ownerCount))
return tecINSUFFICIENT_RESERVE;
}
// Account for the origination fee using two payments // Account for the origination fee using two payments
// //
@@ -359,13 +389,13 @@ LoanSet::doApply()
view, view,
vaultPseudo, vaultPseudo,
borrower, borrower,
STAmount{vaultAsset, loanAssetsAvailable}, STAmount{vaultAsset, loanAssetsToBorrower},
j_, j_,
WaiveTransferFee::Yes)) WaiveTransferFee::Yes))
return ter; return ter;
// 2. Transfer originationFee, if any, from vault pseudo-account to // 2. Transfer originationFee, if any, from vault pseudo-account to
// LoanBroker owner. // LoanBroker owner.
if (originationFee && (*originationFee != Number{})) if (originationFee != Number{})
{ {
// Create the holding if it doesn't already exist (necessary for MPTs). // Create the holding if it doesn't already exist (necessary for MPTs).
// The owner may have deleted their MPT / line at some point. // The owner may have deleted their MPT / line at some point.
@@ -383,31 +413,20 @@ LoanSet::doApply()
view, view,
vaultPseudo, vaultPseudo,
brokerOwner, brokerOwner,
STAmount{vaultAsset, *originationFee}, STAmount{vaultAsset, originationFee},
j_, j_,
WaiveTransferFee::Yes)) WaiveTransferFee::Yes))
return ter; 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 // The portion of the loan interest that will go to the vault (total
// interest minus the management fee) // 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 const startDate = view.info().closeTime.time_since_epoch().count();
auto loanSequence = brokerSle->at(sfLoanSequence); auto loanSequenceProxy = brokerSle->at(sfLoanSequence);
// Create the loan // Create the loan
auto loan = std::make_shared<SLE>(keylet::loan(brokerID, *loanSequence)); auto loan =
std::make_shared<SLE>(keylet::loan(brokerID, *loanSequenceProxy));
// Prevent copy/paste errors // Prevent copy/paste errors
auto setLoanField = auto setLoanField =
@@ -417,12 +436,11 @@ LoanSet::doApply()
loan->at(field) = tx[field].value_or(defValue); loan->at(field) = tx[field].value_or(defValue);
}; };
// Set required tx fields and pre-computed fields // Set required and fixed tx fields
loan->at(sfPrincipalRequested) = principalRequested; loan->at(sfLoanScale) = principalRequested.exponent();
loan->at(sfPrincipalOutstanding) = principalRequested;
loan->at(sfStartDate) = startDate; loan->at(sfStartDate) = startDate;
loan->at(sfPaymentInterval) = paymentInterval; loan->at(sfPaymentInterval) = paymentInterval;
loan->at(sfLoanSequence) = loanSequence; loan->at(sfLoanSequence) = *loanSequenceProxy;
loan->at(sfLoanBrokerID) = brokerID; loan->at(sfLoanBrokerID) = brokerID;
loan->at(sfBorrower) = borrower; loan->at(sfBorrower) = borrower;
// Set all other transaction fields directly from the transaction // Set all other transaction fields directly from the transaction
@@ -438,7 +456,9 @@ LoanSet::doApply()
setLoanField(~sfCloseInterestRate); setLoanField(~sfCloseInterestRate);
setLoanField(~sfOverpaymentInterestRate); setLoanField(~sfOverpaymentInterestRate);
setLoanField(~sfGracePeriod, defaultGracePeriod); 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(sfPreviousPaymentDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = paymentTotal; loan->at(sfPaymentRemaining) = paymentTotal;
@@ -446,7 +466,7 @@ LoanSet::doApply()
// Update the balances in the vault // Update the balances in the vault
vaultSle->at(sfAssetsAvailable) -= principalRequested; vaultSle->at(sfAssetsAvailable) -= principalRequested;
vaultSle->at(sfAssetsTotal) += loanInterestToVault; vaultSle->at(sfAssetsTotal) += totalInterestOwedToVault;
XRPL_ASSERT_PARTS( XRPL_ASSERT_PARTS(
*vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal),
"ripple::LoanSet::doApply", "ripple::LoanSet::doApply",
@@ -454,11 +474,11 @@ LoanSet::doApply()
view.update(vaultSle); view.update(vaultSle);
// Update the balances in the loan broker // 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, // 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 // and is distinct from the broker's pseudo-account's owner count
adjustOwnerCount(view, brokerSle, 1, j_); adjustOwnerCount(view, brokerSle, 1, j_);
loanSequence += 1; loanSequenceProxy += 1;
view.update(brokerSle); view.update(brokerSle);
// Put the loan into the pseudo-account's directory // Put the loan into the pseudo-account's directory