mirror of
https://github.com/XRPLF/rippled.git
synced 2025-11-21 03:26:01 +00:00
documents paymentCOmponents
This commit is contained in:
@@ -895,11 +895,22 @@ PaymentComponents::trackedInterestPart() const
|
|||||||
(trackedPrincipalDelta + trackedManagementFeeDelta);
|
(trackedPrincipalDelta + trackedManagementFeeDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
PaymentComponents
|
/* Computes the breakdown of a regular periodic payment into principal,
|
||||||
computePaymentComponents(
|
* interest, and management fee components.
|
||||||
Asset const& asset,
|
*
|
||||||
std::int32_t scale,
|
* This function determines how a single scheduled payment should be split among
|
||||||
Number const& totalValueOutstanding,
|
* the three tracked loan components. The calculation accounts for accumulated
|
||||||
|
* rounding errors.
|
||||||
|
*
|
||||||
|
* The algorithm:
|
||||||
|
* 1. Calculate what the loan state SHOULD be after this payment (target)
|
||||||
|
* 2. Compare current state to target to get deltas
|
||||||
|
* 3. Adjust deltas to handle rounding artifacts and edge cases
|
||||||
|
* 4. Ensure deltas don't exceed available balances or payment amount
|
||||||
|
*
|
||||||
|
* Special handling for the final payment: all remaining balances are paid off
|
||||||
|
* regardless of the periodic payment amount.
|
||||||
|
*/
|
||||||
PaymentComponents
|
PaymentComponents
|
||||||
computePaymentComponents(
|
computePaymentComponents(
|
||||||
Asset const& asset,
|
Asset const& asset,
|
||||||
@@ -912,10 +923,6 @@ computePaymentComponents(
|
|||||||
std::uint32_t paymentRemaining,
|
std::uint32_t paymentRemaining,
|
||||||
TenthBips16 managementFeeRate)
|
TenthBips16 managementFeeRate)
|
||||||
{
|
{
|
||||||
/*
|
|
||||||
* This function is derived from the XLS-66 spec, section 3.2.4.1.1 (Regular
|
|
||||||
* Payment)
|
|
||||||
*/
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
isRounded(asset, totalValueOutstanding, scale) &&
|
isRounded(asset, totalValueOutstanding, scale) &&
|
||||||
isRounded(asset, principalOutstanding, scale) &&
|
isRounded(asset, principalOutstanding, scale) &&
|
||||||
@@ -926,11 +933,17 @@ computePaymentComponents(
|
|||||||
paymentRemaining > 0,
|
paymentRemaining > 0,
|
||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"some payments remaining");
|
"some payments remaining");
|
||||||
|
|
||||||
auto const roundedPeriodicPayment =
|
auto const roundedPeriodicPayment =
|
||||||
roundPeriodicPayment(asset, periodicPayment, scale);
|
roundPeriodicPayment(asset, periodicPayment, scale);
|
||||||
|
|
||||||
|
// Calculate what the loan state SHOULD be after this payment (the target).
|
||||||
|
// This is computed at full precision using the theoretical amortization.
|
||||||
LoanState const trueTarget = calculateRawLoanState(
|
LoanState const trueTarget = calculateRawLoanState(
|
||||||
periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate);
|
periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate);
|
||||||
|
|
||||||
|
// Round the target to the loan's scale to match how actual loan values
|
||||||
|
// are stored.
|
||||||
LoanState const roundedTarget = LoanState{
|
LoanState const roundedTarget = LoanState{
|
||||||
.valueOutstanding =
|
.valueOutstanding =
|
||||||
roundToAsset(asset, trueTarget.valueOutstanding, scale),
|
roundToAsset(asset, trueTarget.valueOutstanding, scale),
|
||||||
@@ -939,18 +952,25 @@ computePaymentComponents(
|
|||||||
.interestDue = roundToAsset(asset, trueTarget.interestDue, scale),
|
.interestDue = roundToAsset(asset, trueTarget.interestDue, scale),
|
||||||
.managementFeeDue =
|
.managementFeeDue =
|
||||||
roundToAsset(asset, trueTarget.managementFeeDue, scale)};
|
roundToAsset(asset, trueTarget.managementFeeDue, scale)};
|
||||||
|
|
||||||
|
// Get the current actual loan state from the ledger values
|
||||||
LoanState const currentLedgerState = constructRoundedLoanState(
|
LoanState const currentLedgerState = constructRoundedLoanState(
|
||||||
totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
|
totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
|
||||||
|
|
||||||
|
// The difference between current and target states gives us the payment
|
||||||
|
// components. Any discrepancies from accumulated rounding are captured
|
||||||
|
// here.
|
||||||
LoanDeltas deltas = currentLedgerState - roundedTarget;
|
LoanDeltas deltas = currentLedgerState - roundedTarget;
|
||||||
|
|
||||||
|
// Rounding can occasionally produce negative deltas. Zero them out.
|
||||||
deltas.nonNegative();
|
deltas.nonNegative();
|
||||||
|
|
||||||
// Adjust the deltas if necessary for data integrity
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
deltas.principal <= currentLedgerState.principalOutstanding,
|
deltas.principal <= currentLedgerState.principalOutstanding,
|
||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"principal delta not greater than outstanding");
|
"principal delta not greater than outstanding");
|
||||||
|
|
||||||
|
// Cap each component to never exceed what's actually outstanding
|
||||||
deltas.principal =
|
deltas.principal =
|
||||||
std::min(deltas.principal, currentLedgerState.principalOutstanding);
|
std::min(deltas.principal, currentLedgerState.principalOutstanding);
|
||||||
|
|
||||||
@@ -959,6 +979,8 @@ computePaymentComponents(
|
|||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"interest due delta not greater than outstanding");
|
"interest due delta not greater than outstanding");
|
||||||
|
|
||||||
|
// Cap interest to both the outstanding amount AND what's left of the
|
||||||
|
// periodic payment after principal is paid
|
||||||
deltas.interest = std::min(
|
deltas.interest = std::min(
|
||||||
{deltas.interest,
|
{deltas.interest,
|
||||||
std::max(numZero, roundedPeriodicPayment - deltas.principal),
|
std::max(numZero, roundedPeriodicPayment - deltas.principal),
|
||||||
@@ -969,17 +991,18 @@ computePaymentComponents(
|
|||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"management fee due delta not greater than outstanding");
|
"management fee due delta not greater than outstanding");
|
||||||
|
|
||||||
|
// Cap management fee to both the outstanding amount AND what's left of the
|
||||||
|
// periodic payment after principal and interest are paid
|
||||||
deltas.managementFee = std::min(
|
deltas.managementFee = std::min(
|
||||||
{deltas.managementFee,
|
{deltas.managementFee,
|
||||||
roundedPeriodicPayment - (deltas.principal + deltas.interest),
|
roundedPeriodicPayment - (deltas.principal + deltas.interest),
|
||||||
currentLedgerState.managementFeeDue});
|
currentLedgerState.managementFeeDue});
|
||||||
|
|
||||||
|
// Final payment: pay off everything remaining, ignoring the normal
|
||||||
|
// periodic payment amount. This ensures the loan completes cleanly.
|
||||||
if (paymentRemaining == 1 ||
|
if (paymentRemaining == 1 ||
|
||||||
totalValueOutstanding <= roundedPeriodicPayment)
|
totalValueOutstanding <= roundedPeriodicPayment)
|
||||||
{
|
{
|
||||||
// If there's only one payment left, we need to pay off each of the loan
|
|
||||||
// parts.
|
|
||||||
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
deltas.total() <= totalValueOutstanding,
|
deltas.total() <= totalValueOutstanding,
|
||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
@@ -993,7 +1016,6 @@ computePaymentComponents(
|
|||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"last payment management fee agrees");
|
"last payment management fee agrees");
|
||||||
|
|
||||||
// Pay everything off
|
|
||||||
return PaymentComponents{
|
return PaymentComponents{
|
||||||
.trackedValueDelta = totalValueOutstanding,
|
.trackedValueDelta = totalValueOutstanding,
|
||||||
.trackedPrincipalDelta = principalOutstanding,
|
.trackedPrincipalDelta = principalOutstanding,
|
||||||
@@ -1001,34 +1023,34 @@ computePaymentComponents(
|
|||||||
.specialCase = PaymentSpecialCase::final};
|
.specialCase = PaymentSpecialCase::final};
|
||||||
}
|
}
|
||||||
|
|
||||||
// The shortage must never be negative, which indicates that the parts are
|
// Helper to reduce a component by taking from excess
|
||||||
// trying to take more than the whole payment. The excess can be positive,
|
auto const takeFrom = [](Number& component, Number& excess) {
|
||||||
// which indicates that we're not going to take the whole payment amount,
|
|
||||||
// but if so, it must be small.
|
|
||||||
auto takeFrom = [](Number& component, Number& excess) {
|
|
||||||
if (excess > beast::zero)
|
if (excess > beast::zero)
|
||||||
{
|
{
|
||||||
// Take as much of the excess as we can out of the provided part and
|
|
||||||
// the total
|
|
||||||
auto part = std::min(component, excess);
|
auto part = std::min(component, excess);
|
||||||
component -= part;
|
component -= part;
|
||||||
excess -= part;
|
excess -= part;
|
||||||
}
|
}
|
||||||
// If the excess goes negative, we took too much, which should be
|
|
||||||
// impossible
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
excess >= beast::zero,
|
excess >= beast::zero,
|
||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"excess non-negative");
|
"excess non-negative");
|
||||||
};
|
};
|
||||||
auto addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) {
|
|
||||||
// This order is based on where errors are the least problematic
|
// Helper to reduce deltas when they collectively exceed a limit.
|
||||||
|
// Order matters: we prefer to reduce interest first (most flexible),
|
||||||
|
// then management fee, then principal (least flexible).
|
||||||
|
auto const addressExcess = [&takeFrom](LoanDeltas& deltas, Number& excess) {
|
||||||
takeFrom(deltas.interest, excess);
|
takeFrom(deltas.interest, excess);
|
||||||
takeFrom(deltas.managementFee, excess);
|
takeFrom(deltas.managementFee, excess);
|
||||||
takeFrom(deltas.principal, excess);
|
takeFrom(deltas.principal, excess);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check if deltas exceed the total outstanding value. This should never
|
||||||
|
// happen due to earlier caps, but handle it defensively.
|
||||||
Number totalOverpayment =
|
Number totalOverpayment =
|
||||||
deltas.total() - currentLedgerState.valueOutstanding;
|
deltas.total() - currentLedgerState.valueOutstanding;
|
||||||
|
|
||||||
if (totalOverpayment > beast::zero)
|
if (totalOverpayment > beast::zero)
|
||||||
{
|
{
|
||||||
// LCOV_EXCL_START
|
// LCOV_EXCL_START
|
||||||
@@ -1039,7 +1061,7 @@ computePaymentComponents(
|
|||||||
// LCOV_EXCL_STOP
|
// LCOV_EXCL_STOP
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the parts don't add up to too much
|
// Check if deltas exceed the periodic payment amount. Reduce if needed.
|
||||||
Number shortage = roundedPeriodicPayment - deltas.total();
|
Number shortage = roundedPeriodicPayment - deltas.total();
|
||||||
|
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
@@ -1049,21 +1071,21 @@ computePaymentComponents(
|
|||||||
|
|
||||||
if (shortage < beast::zero)
|
if (shortage < beast::zero)
|
||||||
{
|
{
|
||||||
|
// Deltas exceed payment amount - reduce them proportionally
|
||||||
Number excess = -shortage;
|
Number excess = -shortage;
|
||||||
|
|
||||||
addressExcess(deltas, excess);
|
addressExcess(deltas, excess);
|
||||||
|
|
||||||
shortage = -excess;
|
shortage = -excess;
|
||||||
}
|
}
|
||||||
// The shortage should never be negative, which indicates that the
|
|
||||||
// parts are trying to take more than the whole payment. The
|
// At this point, shortage >= 0 means we're paying less than the full
|
||||||
// shortage may be positive, which indicates that we're not going to
|
// periodic payment (due to rounding or component caps).
|
||||||
// take the whole payment amount.
|
// shortage < 0 would mean we're trying to pay more than allowed (bug).
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
shortage >= beast::zero,
|
shortage >= beast::zero,
|
||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"no shortage or excess");
|
"no shortage or excess");
|
||||||
|
|
||||||
|
// Final validation that all components are valid
|
||||||
XRPL_ASSERT_PARTS(
|
XRPL_ASSERT_PARTS(
|
||||||
deltas.total() ==
|
deltas.total() ==
|
||||||
deltas.principal + deltas.interest + deltas.managementFee,
|
deltas.principal + deltas.interest + deltas.managementFee,
|
||||||
@@ -1091,9 +1113,8 @@ computePaymentComponents(
|
|||||||
"ripple::detail::computePaymentComponents",
|
"ripple::detail::computePaymentComponents",
|
||||||
"payment parts add to payment");
|
"payment parts add to payment");
|
||||||
|
|
||||||
|
// Final safety clamp to ensure no value exceeds its outstanding balance
|
||||||
return PaymentComponents{
|
return PaymentComponents{
|
||||||
// As a final safety check, ensure the value is non-negative, and won't
|
|
||||||
// make the corresponding item negative
|
|
||||||
.trackedValueDelta = std::clamp(
|
.trackedValueDelta = std::clamp(
|
||||||
deltas.total(), numZero, currentLedgerState.valueOutstanding),
|
deltas.total(), numZero, currentLedgerState.valueOutstanding),
|
||||||
.trackedPrincipalDelta = std::clamp(
|
.trackedPrincipalDelta = std::clamp(
|
||||||
|
|||||||
Reference in New Issue
Block a user