Merge branch 'ximinez/lending-XLS-66' into ximinez/lending-transitive-amendments

This commit is contained in:
Ed Hennis
2025-11-13 14:46:04 -05:00
committed by GitHub
4 changed files with 1 additions and 406 deletions

View File

@@ -1357,28 +1357,6 @@ protected:
auto const borrowerStartbalance = env.balance(borrower, broker.asset);
auto createJtx = loanParams(env, broker);
#if LOANCOMPLETE
{
auto createJtxOld = env.jt(
set(borrower, broker.brokerID, principalRequest, flags),
sig(sfCounterpartySignature, lender),
loanOriginationFee(originationFee),
loanServiceFee(serviceFee),
latePaymentFee(lateFee),
closePaymentFee(closeFee),
overpaymentFee(overFee),
interestRate(interest),
lateInterestRate(lateInterest),
closeInterestRate(closeInterest),
overpaymentInterestRate(overpaymentInterest),
paymentTotal(total),
paymentInterval(interval),
gracePeriod(grace),
fee(loanSetFee));
BEAST_EXPECT(
createJtx.stx->getJson(0) == createJtxOld.stx->getJson(0));
}
#endif
// Successfully create a Loan
env(createJtx);
@@ -7041,13 +7019,6 @@ class LoanArbitrary_test : public LoanBatch_test
{
using namespace jtx;
#if LOANCOMPLETE
BEAST_EXPECT(
std::numeric_limits<std::int64_t>::max() > INITIAL_XRP.drops());
BEAST_EXPECT(Number::maxMantissa > INITIAL_XRP.drops());
Number initalXrp{INITIAL_XRP};
BEAST_EXPECT(initalXrp.exponent() <= 0);
#endif
BrokerParameters const brokerParams{
.vaultDeposit = 10000,
.debtMax = 0,

View File

@@ -4555,7 +4555,7 @@ class Vault_test : public beast::unit_test::suite
BEAST_EXPECT(checkString(vault, sfAssetsAvailable, "50"));
BEAST_EXPECT(checkString(vault, sfAssetsMaximum, "1000"));
BEAST_EXPECT(checkString(vault, sfAssetsTotal, "50"));
BEAST_EXPECT(checkString(vault, sfLossUnrealized, "0"));
BEAST_EXPECT(!vault.isMember(sfLossUnrealized.getJsonName()));
auto const strShareID = strHex(sle->at(sfShareMPTID));
BEAST_EXPECT(checkString(vault, sfShareMPTID, strShareID));

View File

@@ -164,12 +164,6 @@ enum class PaymentSpecialCase { none, final, extra };
/// single loan payment
struct PaymentComponents
{
#if LOANCOMPLETE
// raw values are unrounded, and are based on pure math
Number rawInterest;
Number rawPrincipal;
Number rawManagementFee;
#endif
// tracked values are rounded to the asset and loan scale, and correspond to
// fields in the Loan ledger object.
// trackedValueDelta modifies sfTotalValueOutstanding.

View File

@@ -219,244 +219,6 @@ loanAccruedInterest(
paymentInterval;
}
#if LOANCOMPLETE
Number
computeRoundedPrincipalComponent(
Asset const& asset,
Number const& principalOutstanding,
Number const& rawPrincipalOutstanding,
Number const& rawPrincipal,
Number const& roundedPeriodicPayment,
std::int32_t scale)
{
// Adjust the principal payment by the rounding error between the true
// and rounded principal outstanding
auto const diff = roundToAsset(
asset,
principalOutstanding - rawPrincipalOutstanding,
scale,
asset.integral() ? Number::downward : Number::towards_zero);
// If the rounded principal outstanding is greater than the true
// principal outstanding, we need to pay more principal to reduce
// the rounded principal outstanding
//
// If the rounded principal outstanding is less than the true
// principal outstanding, we need to pay less principal to allow the
// rounded principal outstanding to catch up
auto const p =
roundToAsset(asset, rawPrincipal + diff, scale, Number::downward);
// For particular loans, it's entirely possible for many of the first
// rounded payments to be all interest.
XRPL_ASSERT_PARTS(
p >= 0,
"rippled::detail::computeRoundedPrincipalComponent",
"principal part not negative");
XRPL_ASSERT_PARTS(
p <= principalOutstanding,
"rippled::detail::computeRoundedPrincipalComponent",
"principal part not larger than outstanding principal");
XRPL_ASSERT_PARTS(
!asset.integral() || abs(p - rawPrincipal) <= 1,
"rippled::detail::computeRoundedPrincipalComponent",
"principal part not larger than outstanding principal");
XRPL_ASSERT_PARTS(
p <= roundedPeriodicPayment,
"rippled::detail::computeRoundedPrincipalComponent",
"principal part not larger than total payment");
// The asserts will be skipped in release builds, so check here to make
// sure nothing goes negative
if (p > roundedPeriodicPayment || p > principalOutstanding)
return std::min(roundedPeriodicPayment, principalOutstanding);
else if (p < 0)
return Number{};
return p;
}
/** Returns the interest component of a payment WITHOUT accounting for
** management fees
*
* In other words, it returns the combined value of the interest part that will
* go to the Vault and the management fee that will go to the Broker.
*/
Number
computeRoundedInterestComponent(
Asset const& asset,
Number const& interestOutstanding,
Number const& roundedPrincipal,
Number const& rawInterestOutstanding,
Number const& roundedPeriodicPayment,
std::int32_t scale)
{
// Start by just using the non-principal part of the payment for interest
Number roundedInterest = roundedPeriodicPayment - roundedPrincipal;
XRPL_ASSERT_PARTS(
isRounded(asset, roundedInterest, scale),
"ripple::detail::computeRoundedInterestComponent",
"initial interest computation is rounded");
{
// Adjust the interest payment by the rounding error between the true
// and rounded interest outstanding
//
// If the rounded interest outstanding is greater than the true interest
// outstanding, we need to pay more interest to reduce the rounded
// interest outstanding
//
// If the rounded interest outstanding is less than the true interest
// outstanding, we need to pay less interest to allow the rounded
// interest outstanding to catch up
auto const diff = roundToAsset(
asset,
interestOutstanding - rawInterestOutstanding,
scale,
asset.integral() ? Number::downward : Number::towards_zero);
roundedInterest += diff;
}
// However, we cannot allow negative interest payments, therefore we need to
// cap the interest payment at 0.
//
// Ensure interest payment is non-negative and does not exceed the remaining
// payment after principal
return std::max(Number{}, roundedInterest);
}
// The Interest and Fee components need to be calculated together, because they
// can affect each other during computation in both directions.
std::pair<Number, Number>
computeRoundedInterestAndFeeComponents(
Asset const& asset,
Number const& interestOutstanding,
Number const& managementFeeOutstanding,
Number const& roundedPrincipal,
Number const& rawInterestOutstanding,
Number const& rawManagementFeeOutstanding,
Number const& roundedPeriodicPayment,
Number const& periodicRate,
TenthBips16 managementFeeRate,
std::int32_t scale)
{
// Zero interest means ZERO interest
if (periodicRate == 0)
return std::make_pair(Number{}, Number{});
Number roundedInterest = computeRoundedInterestComponent(
asset,
interestOutstanding,
roundedPrincipal,
rawInterestOutstanding,
roundedPeriodicPayment,
scale);
Number roundedFee =
computeFee(asset, roundedInterest, managementFeeRate, scale);
{
// Adjust the interest fee by the rounding error between the true and
// rounded interest fee outstanding
auto const diff = roundToAsset(
asset,
managementFeeOutstanding - rawManagementFeeOutstanding,
scale,
asset.integral() ? Number::downward : Number::towards_zero);
roundedFee += diff;
// But again, we cannot allow negative interest fees, therefore we need
// to cap the interest fee at 0
roundedFee = std::max(Number{}, roundedFee);
// Finally, the rounded interest fee cannot exceed the outstanding
// interest fee
roundedFee = std::min(roundedFee, managementFeeOutstanding);
}
// Remove the fee portion from the interest payment, as the fee is paid
// separately
// Ensure that the interest payment does not become negative, this may
// happen with high interest fees
roundedInterest = std::max(Number{}, roundedInterest - roundedFee);
// Finally, ensure that the interest payment does not exceed the
// interest outstanding
roundedInterest = std::min(interestOutstanding, roundedInterest);
// Make sure the parts don't add up to too much
auto const initialTotal = roundedPrincipal + roundedInterest + roundedFee;
Number excess = roundedPeriodicPayment - initialTotal;
XRPL_ASSERT_PARTS(
isRounded(asset, excess, scale),
"ripple::detail::computeRoundedInterestAndFeeComponents",
"excess is rounded");
#if LOANCOMPLETE
if (excess != beast::zero)
std::cout << "computeRoundedInterestAndFeeComponents excess is "
<< excess << std::endl;
#endif
if (excess < beast::zero)
{
// Take as much of the excess as we can out of the interest
#if LOANCOMPLETE
std::cout << "\tApplying excess to interest\n";
#endif
auto part = std::min(roundedInterest, -excess);
roundedInterest -= part;
excess += part;
XRPL_ASSERT_PARTS(
excess <= beast::zero,
"ripple::detail::computeRoundedInterestAndFeeComponents",
"excess not positive (interest)");
}
if (excess < beast::zero)
{
// If there's any left, take as much of the excess as we can out of the
// fee
#if LOANCOMPLETE
std::cout << "\tApplying excess to fee\n";
#endif
auto part = std::min(roundedFee, -excess);
roundedFee -= part;
excess += part;
}
// The excess should never be negative, which indicates that the parts are
// trying to take more than the whole payment. The excess can be positive,
// which indicates that we're not going to take the whole payment amount,
// but if so, it must be small.
XRPL_ASSERT_PARTS(
excess == beast::zero ||
(excess > beast::zero &&
((asset.integral() && excess < 3) ||
(roundedPeriodicPayment.exponent() - excess.exponent() > 6))),
"ripple::detail::computeRoundedInterestAndFeeComponents",
"excess is extremely small (fee)");
XRPL_ASSERT_PARTS(
roundedFee >= beast::zero,
"ripple::detail::computeRoundedInterestAndFeeComponents",
"non-negative fee");
XRPL_ASSERT_PARTS(
roundedInterest >= beast::zero,
"ripple::detail::computeRoundedInterestAndFeeComponents",
"non-negative interest");
return std::make_pair(
std::max(Number{}, roundedInterest), std::max(Number{}, roundedFee));
}
#endif
struct PaymentComponentsPlus : public PaymentComponents
{
// untrackedManagementFeeDelta includes any fees that go directly to the
@@ -846,10 +608,6 @@ computeLatePayment(
view.parentCloseTime(),
nextDueDate);
#if LOANCOMPLETE
auto const [rawLateInterest, rawLateManagementFee] =
computeInterestAndFeeParts(latePaymentInterest, managementFeeRate);
#endif
auto const [roundedLateInterest, roundedLateManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, latePaymentInterest, loanScale);
@@ -868,9 +626,6 @@ computeLatePayment(
// This preserves all the other fields without having to enumerate them.
PaymentComponentsPlus const late = [&]() {
auto inner = periodic;
#if LOANCOMPLETE
inner.rawInterest += rawLateInterest;
#endif
return PaymentComponentsPlus{
inner,
@@ -943,11 +698,6 @@ computeFullPayment(
startDate,
closeInterestRate);
#if LOANCOMPLETE
auto const [rawFullInterest, rawFullManagementFee] =
computeInterestAndFeeParts(fullPaymentInterest, managementFeeRate);
#endif
auto const [roundedFullInterest, roundedFullManagementFee] = [&]() {
auto const interest =
roundToAsset(asset, fullPaymentInterest, loanScale);
@@ -960,11 +710,6 @@ computeFullPayment(
PaymentComponentsPlus const full{
PaymentComponents{
#if LOANCOMPLETE
.rawInterest = rawFullInterest,
.rawPrincipal = rawPrincipalOutstanding,
.rawManagementFee = rawFullManagementFee,
#endif
.trackedValueDelta = principalOutstanding +
totalInterestOutstanding + managementFeeOutstanding,
.trackedPrincipalDelta = principalOutstanding,
@@ -1121,107 +866,12 @@ computePaymentComponents(
// Pay everything off
return PaymentComponents{
#if LOANCOMPLETE
.rawInterest = raw.interestOutstanding,
.rawPrincipal = raw.principalOutstanding,
.rawManagementFee = raw.managementFeeDue,
#endif
.trackedValueDelta = totalValueOutstanding,
.trackedPrincipalDelta = principalOutstanding,
.trackedManagementFeeDelta = managementFeeOutstanding,
.specialCase = PaymentSpecialCase::final};
}
#if LOANCOMPLETE
/*
* From the spec, once the periodicPayment is computed:
*
* The principal and interest portions can be derived as follows:
* interest = principalOutstanding * periodicRate
* principal = periodicPayment - interest
*/
Number const rawInterest = raw.principalOutstanding * periodicRate;
Number const rawPrincipal = periodicPayment - rawInterest;
Number const rawFee = tenthBipsOfValue(rawInterest, managementFeeRate);
XRPL_ASSERT_PARTS(
rawInterest >= 0,
"ripple::detail::computePaymentComponents",
"valid raw interest");
XRPL_ASSERT_PARTS(
rawPrincipal >= 0 && rawPrincipal <= raw.principalOutstanding,
"ripple::detail::computePaymentComponents",
"valid raw principal");
XRPL_ASSERT_PARTS(
rawFee >= 0 && rawFee <= raw.managementFeeDue,
"ripple::detail::computePaymentComponents",
"valid raw fee");
/*
Critical Calculation: Balancing Principal and Interest Outstanding
This calculation maintains a delicate balance between keeping
principal outstanding and interest outstanding as close as possible to
reference values. However, we cannot perfectly match the reference
values due to rounding issues.
Key considerations:
1. Since the periodic payment is rounded up, we have excess funds
that can be used to pay down the loan faster than the reference
calculation.
2. We must ensure that loan repayment is not too fast, otherwise we
will end up with negative principal outstanding or negative
interest outstanding.
3. We cannot allow the borrower to repay interest ahead of schedule.
If the borrower makes an overpayment, the interest portion could
go negative, requiring complex recalculation to refund the borrower by
reflecting the overpayment in the principal portion of the loan.
*/
Number const roundedPrincipal = detail::computeRoundedPrincipalComponent(
asset,
principalOutstanding,
raw.principalOutstanding,
rawPrincipal,
roundedPeriodicPayment,
scale);
auto const [roundedInterest, roundedFee] =
detail::computeRoundedInterestAndFeeComponents(
asset,
totalValueOutstanding - principalOutstanding,
managementFeeOutstanding,
roundedPrincipal,
raw.interestOutstanding,
raw.managementFeeDue,
roundedPeriodicPayment,
periodicRate,
managementFeeRate,
scale);
XRPL_ASSERT_PARTS(
roundedInterest >= 0 && isRounded(asset, roundedInterest, scale),
"ripple::detail::computePaymentComponents",
"valid rounded interest");
XRPL_ASSERT_PARTS(
roundedFee >= 0 && roundedFee <= managementFeeOutstanding &&
isRounded(asset, roundedFee, scale),
"ripple::detail::computePaymentComponents",
"valid rounded fee");
XRPL_ASSERT_PARTS(
roundedPrincipal >= 0 && roundedPrincipal <= principalOutstanding &&
roundedPrincipal <= roundedPeriodicPayment &&
isRounded(asset, roundedPrincipal, scale),
"ripple::detail::computePaymentComponents",
"valid rounded principal");
XRPL_ASSERT_PARTS(
roundedPrincipal + roundedInterest + roundedFee <=
roundedPeriodicPayment,
"ripple::detail::computePaymentComponents",
"payment parts fit within payment limit");
#endif
// The shortage must never be negative, which indicates that the parts are
// trying to take more than the whole payment. The excess can be positive,
// which indicates that we're not going to take the whole payment amount,
@@ -1284,16 +934,6 @@ computePaymentComponents(
shortage >= beast::zero,
"ripple::detail::computePaymentComponents",
"no shortage or excess");
#if LOANCOMPLETE
/*
// This used to be part of the above assert. It will eventually be removed
// if proved accurate
||
(shortage > beast::zero &&
((asset.integral() && shortage < 3) ||
(scale - shortage.exponent() > 14)))
*/
#endif
XRPL_ASSERT_PARTS(
deltas.valueDelta() ==
@@ -1326,11 +966,6 @@ computePaymentComponents(
"payment parts add to payment");
return PaymentComponents{
#if LOANCOMPLETE
.rawInterest = rawInterest - rawFee,
.rawPrincipal = rawPrincipal,
.rawManagementFee = rawFee,
#endif
// As a final safety check, ensure the value is non-negative, and won't
// make the corresponding item negative
.trackedValueDelta = std::clamp(
@@ -1380,11 +1015,6 @@ computeOverpaymentComponents(
return detail::PaymentComponentsPlus{
detail::PaymentComponents{
#if LOANCOMPLETE
.rawInterest = rawOverpaymentInterest,
.rawPrincipal = payment - rawOverpaymentInterest,
.rawManagementFee = 0,
#endif
.trackedValueDelta = payment,
.trackedPrincipalDelta = payment - roundedOverpaymentInterest -
roundedOverpaymentManagementFee,