xrpld
Loading...
Searching...
No Matches
LendingHelpers.cpp
1#include <xrpl/ledger/helpers/LendingHelpers.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/basics/Number.h>
5#include <xrpl/basics/chrono.h>
6#include <xrpl/beast/utility/Journal.h>
7#include <xrpl/beast/utility/Zero.h>
8#include <xrpl/beast/utility/instrumentation.h>
9#include <xrpl/ledger/ApplyView.h>
10#include <xrpl/ledger/ReadView.h>
11#include <xrpl/ledger/View.h>
12#include <xrpl/protocol/Asset.h>
13#include <xrpl/protocol/Feature.h>
14#include <xrpl/protocol/LedgerFormats.h>
15#include <xrpl/protocol/Protocol.h>
16#include <xrpl/protocol/Rules.h>
17#include <xrpl/protocol/SField.h>
18#include <xrpl/protocol/STAmount.h>
19#include <xrpl/protocol/STLedgerEntry.h>
20#include <xrpl/protocol/STTx.h>
21#include <xrpl/protocol/TER.h>
22#include <xrpl/protocol/Units.h>
23
24#include <algorithm>
25#include <cstddef>
26#include <cstdint>
27#include <expected>
28#include <string_view>
29#include <utility>
30
31namespace xrpl {
32
33[[nodiscard]] TER
35 ReadView const& view,
36 SLE::const_ref sleBroker,
37 Asset const& vaultAsset,
38 STAmount const& amount,
40 std::string_view logPrefix)
41{
42 XRPL_ASSERT(
43 sleBroker && sleBroker->getType() == ltLOAN_BROKER,
44 "xrpl::canApplyToBrokerCover : valid LoanBroker sle");
45 XRPL_ASSERT(vaultAsset == amount.asset(), "xrpl::canApplyToBrokerCover : valid asset");
46
47 if (!view.rules().enabled(fixCleanup3_2_0))
48 return tesSUCCESS;
49
50 if (amount == beast::kZero)
51 return tecPRECISION_LOSS;
52
53 int const coverScale = scale(sleBroker->at(sfCoverAvailable), vaultAsset);
54 if (amount.isZeroAtScale(coverScale))
55 {
56 JLOG(j.warn()) << logPrefix << ": amount " << amount.getFullText()
57 << " rounds to zero at cover scale " << coverScale;
58 return tecPRECISION_LOSS;
59 }
60
61 return tesSUCCESS;
62}
63
64bool
66{
67 if (!rules.enabled(featureSingleAssetVault))
68 return false;
69
70 if (!rules.enabled(featureMPTokensV1))
71 return false;
72
73 if (tx.isFieldPresent(sfDomainID) && !rules.enabled(featurePermissionedDomains))
74 return false;
75
76 return true;
77}
78
79LoanPaymentParts&
81{
82 XRPL_ASSERT(
83
84 other.principalPaid >= beast::kZero,
85 "xrpl::LoanPaymentParts::operator+= : other principal "
86 "non-negative");
87 XRPL_ASSERT(
88 other.interestPaid >= beast::kZero,
89 "xrpl::LoanPaymentParts::operator+= : other interest paid "
90 "non-negative");
91 XRPL_ASSERT(
92 other.feePaid >= beast::kZero,
93 "xrpl::LoanPaymentParts::operator+= : other fee paid "
94 "non-negative");
95
98 valueChange += other.valueChange;
99 feePaid += other.feePaid;
100 return *this;
101}
102
103bool
105{
106 return principalPaid == other.principalPaid && interestPaid == other.interestPaid &&
107 valueChange == other.valueChange && feePaid == other.feePaid;
108}
109
110/* Converts annualized interest rate to per-payment-period rate.
111 * The rate is prorated based on the payment interval in seconds.
112 *
113 * Equation (1) from XLS-66 spec, Section A-2 Equation Glossary
114 */
115Number
116loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
117{
118 // Need floating point math, since we're dividing by a large number
119 return tenthBipsOfValue(Number(paymentInterval), interestRate) / kSecondsInYear;
120}
121
122/* Checks if a value is already rounded to the specified scale.
123 * Returns true if rounding down and rounding up produce the same result,
124 * indicating no further precision exists beyond the scale.
125 */
126bool
127isRounded(Asset const& asset, Number const& value, std::int32_t scale)
128{
129 return roundToAsset(asset, value, scale, Number::RoundingMode::Downward) ==
131}
132
133namespace detail {
134
135void
137{
138 if (principal < beast::kZero)
140 if (interest < beast::kZero)
142 if (managementFee < beast::kZero)
144}
145
146/* Computes (1 + r)^n - 1 accurately even for near-zero r, where direct
147 * subtraction of `power(1 + r, n) - 1` suffers catastrophic cancellation.
148 *
149 * The binomial expansion gives
150 * (1 + r)^n - 1 = sum_{k=1}^{n} C(n,k) r^k
151 * = nr + C(n,2) r^2 + ... + r^n
152 * which is a sum of positive terms when r >= 0, avoiding cancellation.
153 * Each term is computed from the previous via
154 * term_{k+1} = term_k * r * (n - k) / (k + 1)
155 *
156 * The loop terminates early once the next term is below Number precision.
157 */
158Number
159computePowerMinusOne(Number const& periodicRate, std::uint32_t paymentsRemaining)
160{
161 XRPL_ASSERT_PARTS(
162 periodicRate >= beast::kZero,
163 "xrpl::detail::computePowerMinusOne",
164 "periodicRate is non-negative");
165
166 if (paymentsRemaining == 0 || periodicRate == beast::kZero)
167 return kNumZero;
168
169 // k = 1 term: C(n, 1) * r = n * r
170 Number term = paymentsRemaining * periodicRate;
171 Number sum = term;
172 for (std::uint32_t k = 1; k < paymentsRemaining; ++k)
173 {
174 // term_{k+1} from term_k: multiply by r * (n - k) / (k + 1)
175 term = term * periodicRate * (paymentsRemaining - k) / (k + 1);
176 Number const next = sum + term;
177 // adding this term fell below Number's precision
178 if (next == sum)
179 break;
180 sum = next;
181 }
182 return sum;
183}
184
185/* Hybrid evaluator of (1 + r)^n - 1.
186 *
187 * The closed-form `power(1 + r, n) - 1` loses sig digits to cancellation
188 * when `r * n` is small: the result `~r*n` sits well below the `1` that
189 * dominates `(1+r)^n`, so most of Number's stored precision is consumed
190 * by the leading `1`.
191 *
192 * A threshold of `1e-9` preserves the closed-form path for any rate the
193 * lending code actually sees in practice (fixtures at moderate rates are bit-exact),
194 * while routing the pathological near-zero regime through the binomial
195 * expansion where cancellation is severe.
196 */
197Number
198computePowerMinusOneHybrid(Number const& periodicRate, std::uint32_t paymentsRemaining)
199{
200 XRPL_ASSERT_PARTS(
201 periodicRate >= beast::kZero,
202 "xrpl::detail::computePowerMinusOneHybrid",
203 "periodicRate is non-negative");
204
205 if (paymentsRemaining == 0 || periodicRate == beast::kZero)
206 return kNumZero;
207
208 // Threshold 1e-9 retains ~10 sig digits of (1+r)^n - 1 against
209 // Number's 19-digit mantissa: the leading "1" of (1+r)^n consumes
210 // ~log10(1/(r*n)) digits before the subtraction. Above this point
211 // closed form is accurate and ~30-500x faster than the binomial
212 // expansion.
213 Number const cancellationThreshold{1, -9};
214 if (paymentsRemaining * periodicRate >= cancellationThreshold)
215 return power(1 + periodicRate, paymentsRemaining) - 1;
216
217 return computePowerMinusOne(periodicRate, paymentsRemaining);
218}
219
220/* Computes the payment factor used in standard amortization formulas.
221 * This factor converts principal to periodic payment amount.
222 *
223 * Equation (6) from XLS-66 spec, Section A-2 Equation Glossary
224 */
225Number
227 Rules const& rules,
228 Number const& periodicRate,
229 std::uint32_t paymentsRemaining)
230{
231 if (paymentsRemaining == 0)
232 return kNumZero;
233
234 // For zero interest, payment factor is simply 1/paymentsRemaining
235 if (periodicRate == beast::kZero)
236 return Number{1} / paymentsRemaining;
237
238 if (rules.enabled(fixCleanup3_2_0))
239 {
240 Number const raisedRateMinusOne =
241 computePowerMinusOneHybrid(periodicRate, paymentsRemaining);
242 Number const raisedRate = 1 + raisedRateMinusOne;
243
244 return (periodicRate * raisedRate) / raisedRateMinusOne;
245 }
246
247 // Pre-fixCleanup3_2_0: direct subtraction `(1+r)^n - 1` suffers
248 // catastrophic cancellation at near-zero rates. Retained for
249 // amendment-gated bit-exact pre-fix behavior.
250 Number const raisedRate = power(1 + periodicRate, paymentsRemaining);
251
252 return (periodicRate * raisedRate) / (raisedRate - 1);
253}
254
255/* Calculates the periodic payment amount using standard amortization formula.
256 * For interest-free loans, returns principal divided equally across payments.
257 *
258 * Equation (7) from XLS-66 spec, Section A-2 Equation Glossary
259 */
260Number
262 Rules const& rules,
263 Number const& principalOutstanding,
264 Number const& periodicRate,
265 std::uint32_t paymentsRemaining)
266{
267 if (principalOutstanding == 0 || paymentsRemaining == 0)
268 return 0;
269
270 // Interest-free loans: equal principal payments
271 if (periodicRate == beast::kZero)
272 return principalOutstanding / paymentsRemaining;
273
274 return principalOutstanding * computePaymentFactor(rules, periodicRate, paymentsRemaining);
275}
276
277/* Reverse-calculates principal from periodic payment amount.
278 * Used to determine theoretical principal at any point in the schedule.
279 *
280 * Equation (10) from XLS-66 spec, Section A-2 Equation Glossary
281 */
282Number
284 Rules const& rules,
285 Number const& periodicPayment,
286 Number const& periodicRate,
287 std::uint32_t paymentsRemaining)
288{
289 if (paymentsRemaining == 0)
290 return kNumZero;
291
292 if (periodicRate == 0)
293 return periodicPayment * paymentsRemaining;
294
295 return periodicPayment / computePaymentFactor(rules, periodicRate, paymentsRemaining);
296}
297
298/*
299 * Computes the interest and management fee parts from interest amount.
300 *
301 * Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
302 */
305 Asset const& asset,
306 Number const& interest,
307 TenthBips16 managementFeeRate,
308 std::int32_t loanScale)
309{
310 auto const fee = computeManagementFee(asset, interest, managementFeeRate, loanScale);
311
312 return std::make_pair(interest - fee, fee);
313}
314
315/* Calculates penalty interest accrued on overdue payments.
316 * Returns 0 if payment is not late.
317 *
318 * Equation (16) from XLS-66 spec, Section A-2 Equation Glossary
319 */
320Number
322 Number const& principalOutstanding,
323 TenthBips32 lateInterestRate,
324 NetClock::time_point parentCloseTime,
325 std::uint32_t nextPaymentDueDate)
326{
327 if (principalOutstanding == beast::kZero)
328 return kNumZero;
329
330 if (lateInterestRate == TenthBips32{0})
331 return kNumZero;
332
333 auto const now = parentCloseTime.time_since_epoch().count();
334
335 // If the payment is not late by any amount of time, then there's no late
336 // interest
337 if (now <= nextPaymentDueDate)
338 return 0;
339
340 // Equation (3) from XLS-66 spec, Section A-2 Equation Glossary
341 auto const secondsOverdue = now - nextPaymentDueDate;
342
343 auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue);
344
345 return principalOutstanding * rate;
346}
347
348/* Calculates interest accrued since the last payment based on time elapsed.
349 * Returns 0 if loan is paid ahead of schedule.
350 *
351 * Equation (27) from XLS-66 spec, Section A-2 Equation Glossary
352 */
353Number
355 Number const& principalOutstanding,
356 Number const& periodicRate,
357 NetClock::time_point parentCloseTime,
358 std::uint32_t startDate,
359 std::uint32_t prevPaymentDate,
360 std::uint32_t paymentInterval)
361{
362 if (periodicRate == beast::kZero)
363 return kNumZero;
364
365 if (paymentInterval == 0)
366 return kNumZero;
367
368 auto const lastPaymentDate = std::max(prevPaymentDate, startDate);
369 auto const now = parentCloseTime.time_since_epoch().count();
370
371 // If the loan has been paid ahead, then "lastPaymentDate" is in the future,
372 // and no interest has accrued.
373 if (now <= lastPaymentDate)
374 return kNumZero;
375
376 // Equation (4) from XLS-66 spec, Section A-2 Equation Glossary
377 auto const secondsSinceLastPayment = now - lastPaymentDate;
378
379 // Division is more likely to introduce rounding errors, which will then get
380 // amplified by multiplication. Therefore, we first multiply, and only then
381 // divide.
382 return principalOutstanding * periodicRate * secondsSinceLastPayment / paymentInterval;
383}
384
385/* Applies a payment to the loan state and returns the breakdown of amounts
386 * paid.
387 *
388 * This is the core function that updates the Loan ledger object fields based on
389 * a computed payment.
390
391 * The function is templated to work with both direct Number/uint32_t values
392 * (for testing/simulation) and ValueProxy types (for actual ledger updates).
393 */
394template <class NumberProxy, class UInt32Proxy, class UInt32OptionalProxy>
397 ExtendedPaymentComponents const& payment,
398 NumberProxy& totalValueOutstandingProxy,
399 NumberProxy& principalOutstandingProxy,
400 NumberProxy& managementFeeOutstandingProxy,
401 UInt32Proxy& paymentRemainingProxy,
402 UInt32Proxy& prevPaymentDateProxy,
403 UInt32OptionalProxy& nextDueDateProxy,
404 std::uint32_t paymentInterval)
405{
406 XRPL_ASSERT_PARTS(nextDueDateProxy, "xrpl::detail::doPayment", "Next due date proxy set");
407
409 {
410 XRPL_ASSERT_PARTS(
411 principalOutstandingProxy == payment.trackedPrincipalDelta,
412 "xrpl::detail::doPayment",
413 "Full principal payment");
414 XRPL_ASSERT_PARTS(
415 totalValueOutstandingProxy == payment.trackedValueDelta,
416 "xrpl::detail::doPayment",
417 "Full value payment");
418 XRPL_ASSERT_PARTS(
419 managementFeeOutstandingProxy == payment.trackedManagementFeeDelta,
420 "xrpl::detail::doPayment",
421 "Full management fee payment");
422
423 // Mark the loan as complete
424 paymentRemainingProxy = 0;
425
426 // Record when the final payment was made
427 prevPaymentDateProxy = *nextDueDateProxy;
428
429 // Clear the next due date. Setting it to 0 causes
430 // it to be removed from the Loan ledger object, saving space.
431 nextDueDateProxy = 0;
432
433 // Zero out all tracked loan balances to mark the loan as paid off.
434 // These will be removed from the Loan object since they're default
435 // values.
436 principalOutstandingProxy = 0;
437 totalValueOutstandingProxy = 0;
438 managementFeeOutstandingProxy = 0;
439 }
440 else
441 {
442 // For regular payments (not overpayments), advance the payment schedule
444 {
445 paymentRemainingProxy -= 1;
446
447 prevPaymentDateProxy = nextDueDateProxy;
448 nextDueDateProxy += paymentInterval;
449 }
450 XRPL_ASSERT_PARTS(
451 principalOutstandingProxy > payment.trackedPrincipalDelta,
452 "xrpl::detail::doPayment",
453 "Partial principal payment");
454 XRPL_ASSERT_PARTS(
455 totalValueOutstandingProxy > payment.trackedValueDelta,
456 "xrpl::detail::doPayment",
457 "Partial value payment");
458 // Management fees are expected to be relatively small, and could get to
459 // zero before the loan is paid off
460 XRPL_ASSERT_PARTS(
461 managementFeeOutstandingProxy >= payment.trackedManagementFeeDelta,
462 "xrpl::detail::doPayment",
463 "Valid management fee");
464
465 // Apply the payment deltas to reduce the outstanding balances
466 principalOutstandingProxy -= payment.trackedPrincipalDelta;
467 totalValueOutstandingProxy -= payment.trackedValueDelta;
468 managementFeeOutstandingProxy -= payment.trackedManagementFeeDelta;
469 }
470
471 // Principal can never exceed total value (principal is part of total value)
472 XRPL_ASSERT_PARTS(
473 // Use an explicit cast because the template parameter can be
474 // ValueProxy<Number> or Number
475 static_cast<Number>(principalOutstandingProxy) <=
476 static_cast<Number>(totalValueOutstandingProxy),
477 "xrpl::detail::doPayment",
478 "principal does not exceed total");
479
480 XRPL_ASSERT_PARTS(
481 // Use an explicit cast because the template parameter can be
482 // ValueProxy<Number> or Number
483 static_cast<Number>(managementFeeOutstandingProxy) >= beast::kZero,
484 "xrpl::detail::doPayment",
485 "fee outstanding stays valid");
486
487 return LoanPaymentParts{
488 // Principal paid is straightforward - it's the tracked delta
489 .principalPaid = payment.trackedPrincipalDelta,
490
491 // Interest paid combines:
492 // 1. Tracked interest from the amortization schedule
493 // (derived from the tracked deltas)
494 // 2. Untracked interest (e.g., late payment penalties)
495 .interestPaid = payment.trackedInterestPart() + payment.untrackedInterest,
496
497 // Value change represents how the loan's total value changed beyond
498 // normal amortization.
499 .valueChange = payment.untrackedInterest,
500
501 // Fee paid combines:
502 // 1. Tracked management fees from the amortization schedule
503 // 2. Untracked fees (e.g., late payment fees, service fees)
504 .feePaid = payment.trackedManagementFeeDelta + payment.untrackedManagementFee};
505}
506
507/* Simulates an overpayment to validate it won't break the loan's amortization.
508 *
509 * When a borrower pays more than the scheduled amount, the loan needs to be
510 * re-amortized with a lower principal. This function performs that calculation
511 * in a "sandbox" using temporary variables, allowing the caller to validate
512 * the result before committing changes to the actual ledger.
513 *
514 * The function preserves accumulated rounding errors across the re-amortization
515 * to ensure the loan state remains consistent with its payment history.
516 */
517std::expected<std::pair<LoanPaymentParts, LoanProperties>, TER>
519 Rules const& rules,
520 Asset const& asset,
521 std::int32_t loanScale,
522 ExtendedPaymentComponents const& overpaymentComponents,
523 LoanState const& roundedOldState,
524 Number const& periodicPayment,
525 Number const& periodicRate,
526 std::uint32_t paymentRemaining,
527 TenthBips16 const managementFeeRate,
529{
530 // Calculate what the loan state SHOULD be theoretically (at full precision)
531 auto const theoreticalState = computeTheoreticalLoanState(
532 rules, periodicPayment, periodicRate, paymentRemaining, managementFeeRate);
533
534 // Calculate the accumulated rounding errors. These need to be preserved
535 // across the re-amortization to maintain consistency with the loan's
536 // payment history. Without preserving these errors, the loan could end
537 // up with a different total value than what the borrower has actually paid.
538 auto const errors = roundedOldState - theoreticalState;
539
540 // Compute the new principal by applying the overpayment to the theoretical
541 // principal. Use max with 0 to ensure we never go negative.
542 auto const newTheoreticalPrincipal = std::max(
543 theoreticalState.principalOutstanding - overpaymentComponents.trackedPrincipalDelta,
544 Number{0});
545
546 // Compute new loan properties based on the reduced principal. This
547 // recalculates the periodic payment, total value, and management fees
548 // for the remaining payment schedule.
549 auto newLoanProperties = computeLoanProperties(
550 rules,
551 asset,
552 newTheoreticalPrincipal,
553 periodicRate,
554 paymentRemaining,
555 managementFeeRate,
556 loanScale);
557
558 JLOG(j.debug()) << "new periodic payment: " << newLoanProperties.periodicPayment
559 << ", new total value: " << newLoanProperties.loanState.valueOutstanding
560 << ", first payment principal: " << newLoanProperties.firstPaymentPrincipal;
561
562 // Calculate what the new loan state should be with the new periodic payment,
563 // including the preserved rounding errors.
564
565 auto const newTheoreticalState = [&]() {
566 auto const state = computeTheoreticalLoanState(
567 rules,
568 newLoanProperties.periodicPayment,
569 periodicRate,
570 paymentRemaining,
571 managementFeeRate) +
572 errors;
573
574 if (!rules.enabled(fixCleanup3_2_0))
575 return state;
576
577 // The new principal is known exactly: it is reduced by the overpayment's
578 // principal portion. computeTheoreticalLoanState instead derives the
579 // principal -- and, from it, the management fee and interest -- via a
580 // lossy (P * factor) / factor round-trip. Pin the principal to the exact
581 // value and re-derive the management fee from the exact interest gross
582 // (value - principal), so the intermediate state is fully consistent with
583 // the exact principal rather than the one-scale-unit-high round-trip.
584 Number const principal =
585 roundedOldState.principalOutstanding - overpaymentComponents.trackedPrincipalDelta;
586 Number const managementFee =
587 tenthBipsOfValue(state.valueOutstanding - principal, managementFeeRate);
588 return constructLoanState(state.valueOutstanding, principal, managementFee);
589 }();
590
591 JLOG(j.debug()) << "new theoretical value: " << newTheoreticalState.valueOutstanding
592 << ", principal: " << newTheoreticalState.principalOutstanding
593 << ", interest gross: " << newTheoreticalState.interestOutstanding();
594
595 // Update the loan state variables with the new values that include the
596 // preserved rounding errors. This ensures the loan's tracked state remains
597 // consistent with its payment history.
598 auto const principalOutstanding = std::clamp(
600 asset,
601 newTheoreticalState.principalOutstanding,
602 loanScale,
604 kNumZero,
605 roundedOldState.principalOutstanding);
606 auto const totalValueOutstanding = std::clamp(
608 asset,
609 principalOutstanding + newTheoreticalState.interestOutstanding(),
610 loanScale,
612 kNumZero,
613 roundedOldState.valueOutstanding);
614 auto const managementFeeOutstanding = std::clamp(
615 roundToAsset(asset, newTheoreticalState.managementFeeDue, loanScale),
616 kNumZero,
617 roundedOldState.managementFeeDue);
618
619 auto const roundedNewState =
620 constructLoanState(totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
621
622 // Update newLoanProperties so that checkLoanGuards can make an accurate
623 // evaluation.
624 newLoanProperties.loanState = roundedNewState;
625
626 JLOG(j.debug()) << "new rounded value: " << roundedNewState.valueOutstanding
627 << ", principal: " << roundedNewState.principalOutstanding
628 << ", interest gross: " << roundedNewState.interestOutstanding();
629
630 // check that the loan is still valid
631 if (auto const ter = checkLoanGuards(
632 asset,
633 principalOutstanding,
634 // The loan may have been created with interest, but for
635 // small interest amounts, that may have already been paid
636 // off. Check what's still outstanding. This should
637 // guarantee that the interest checks pass.
638 roundedNewState.interestOutstanding() != beast::kZero,
639 paymentRemaining,
640 newLoanProperties,
641 j))
642 {
643 JLOG(j.warn()) << "Principal overpayment would cause the loan to be in "
644 "an invalid state. Ignore the overpayment";
645
647 }
648
649 // Validate that all computed properties are reasonable. These checks should
650 // never fail under normal circumstances, but we validate defensively.
651 if (newLoanProperties.periodicPayment <= 0 ||
652 newLoanProperties.loanState.valueOutstanding <= 0 ||
653 newLoanProperties.loanState.managementFeeDue < 0)
654 {
655 // LCOV_EXCL_START
656 JLOG(j.warn()) << "Overpayment not allowed: Computed loan "
657 "properties are invalid. Does "
658 "not compute. TotalValueOutstanding: "
659 << newLoanProperties.loanState.valueOutstanding
660 << ", PeriodicPayment : " << newLoanProperties.periodicPayment
661 << ", ManagementFeeOwedToBroker: "
662 << newLoanProperties.loanState.managementFeeDue;
664 // LCOV_EXCL_STOP
665 }
666
667 auto const deltas = roundedOldState - roundedNewState;
668
669 // The change in loan management fee is equal to the change between the old
670 // and the new outstanding management fees
671 XRPL_ASSERT_PARTS(
672 deltas.managementFee == roundedOldState.managementFeeDue - managementFeeOutstanding,
673 "xrpl::detail::tryOverpayment",
674 "no fee change");
675
676 // Calculate how the loan's value changed due to the overpayment.
677 // This should be negative (value decreased) or zero. A principal
678 // overpayment should never increase the loan's value.
679 // The value change is derived from the reduction in interest due to
680 // the lower principal.
681 // We do not consider the change in management fee here, since
682 // management fees are excluded from the valueOutstanding.
683 auto const valueChange = -deltas.interest;
684 if (valueChange > 0)
685 {
686 JLOG(j.warn()) << "Principal overpayment would increase the value of "
687 "the loan. Ignore the overpayment";
689 }
690
691 return std::make_pair(
693 // Principal paid is the reduction in principal outstanding
694 .principalPaid = deltas.principal,
695 // Interest paid is the reduction in interest due
696 .interestPaid = overpaymentComponents.untrackedInterest,
697 // Value change includes both the reduction from paying down
698 // principal (negative) and any untracked interest penalties
699 // (positive, e.g., if the overpayment itself incurs a fee)
700 .valueChange = valueChange + overpaymentComponents.untrackedInterest,
701 // Fee paid includes both the reduction in tracked management fees
702 // and any untracked fees on the overpayment itself
703 .feePaid = overpaymentComponents.untrackedManagementFee +
704 overpaymentComponents.trackedManagementFeeDelta,
705 },
706 newLoanProperties);
707}
708
709/* Validates and applies an overpayment to the loan state.
710 *
711 * This function acts as a wrapper around tryOverpayment(), performing the
712 * re-amortization calculation in a sandbox (using temporary copies of the
713 * loan state), then validating the results before committing them to the
714 * actual ledger via the proxy objects.
715 *
716 * The two-step process (try in sandbox, then commit) ensures that if the
717 * overpayment would leave the loan in an invalid state, we can reject it
718 * gracefully without corrupting the ledger data.
719 */
720template <class NumberProxy>
721std::expected<LoanPaymentParts, TER>
723 Rules const& rules,
724 Asset const& asset,
725 std::int32_t loanScale,
726 ExtendedPaymentComponents const& overpaymentComponents,
727 NumberProxy& totalValueOutstandingProxy,
728 NumberProxy& principalOutstandingProxy,
729 NumberProxy& managementFeeOutstandingProxy,
730 NumberProxy& periodicPaymentProxy,
731 Number const& periodicRate,
732 std::uint32_t const paymentRemaining,
733 TenthBips16 const managementFeeRate,
735{
736 auto const loanState = constructLoanState(
737 totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy);
738 auto const periodicPayment = periodicPaymentProxy;
739 JLOG(j.debug()) << "overpayment components:"
740 << ", totalValue before: " << *totalValueOutstandingProxy
741 << ", valueDelta: " << overpaymentComponents.trackedValueDelta
742 << ", principalDelta: " << overpaymentComponents.trackedPrincipalDelta
743 << ", managementFeeDelta: " << overpaymentComponents.trackedManagementFeeDelta
744 << ", interestPart: " << overpaymentComponents.trackedInterestPart()
745 << ", untrackedInterest: " << overpaymentComponents.untrackedInterest
746 << ", totalDue: " << overpaymentComponents.totalDue
747 << ", payments remaining :" << paymentRemaining;
748
749 // Attempt to re-amortize the loan with the overpayment applied.
750 // This modifies the temporary copies, leaving the proxies unchanged.
751 auto const ret = tryOverpayment(
752 rules,
753 asset,
754 loanScale,
755 overpaymentComponents,
756 loanState,
757 periodicPayment,
758 periodicRate,
759 paymentRemaining,
760 managementFeeRate,
761 j);
762 if (!ret)
763 return std::unexpected(ret.error());
764
765 auto const& [loanPaymentParts, newLoanProperties] = *ret;
766 auto const newRoundedLoanState = newLoanProperties.loanState;
767
768 // Safety check: the principal must have decreased. If it didn't (or
769 // increased!), something went wrong in the calculation and we should
770 // reject the overpayment.
771 if (principalOutstandingProxy <= newRoundedLoanState.principalOutstanding)
772 {
773 // LCOV_EXCL_START
774 JLOG(j.warn()) << "Overpayment not allowed: principal "
775 << "outstanding did not decrease. Before: " << *principalOutstandingProxy
776 << ". After: " << newRoundedLoanState.principalOutstanding;
778 // LCOV_EXCL_STOP
779 }
780
781 // The proxies still hold the original (pre-overpayment) values, which
782 // allows us to compute deltas and verify they match what we expect
783 // from the overpaymentComponents and loanPaymentParts.
784 JLOG(j.debug()) << "valueChange: " << loanPaymentParts.valueChange
785 << ", totalValue before: " << *totalValueOutstandingProxy
786 << ", totalValue after: " << newRoundedLoanState.valueOutstanding
787 << ", totalValue delta: "
788 << (totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding)
789 << ", principalDelta: " << overpaymentComponents.trackedPrincipalDelta
790 << ", principalPaid: " << loanPaymentParts.principalPaid
791 << ", Computed difference: "
792 << overpaymentComponents.trackedPrincipalDelta -
793 (totalValueOutstandingProxy - newRoundedLoanState.valueOutstanding);
794
795 // The three assertions below are invariants that only hold once
796 // fixCleanup3_2_0 pins the new principal to the exact reduction
797 // (oldPrincipal - trackedPrincipalDelta). Before the amendment, the lossy
798 // (P * factor) / factor round-trip can leave the new principal one
799 // scale-unit high, so these equalities do not hold on the pre-amendment
800 // code path and must be gated to match the fix they verify.
801 //
802 // The valueChange returned by tryOverpayment satisfies
803 // valueChange = (newInterestDue - oldInterestDue) + untrackedInterest.
804 // Using the loan-state identity v = p + i + m and the adjacent
805 // `principal change agrees` assertion (dp = oldP - newP), this
806 // rearranges into three independently-computable terms:
807 //
808 // 1. TVO change beyond what principal repayment alone explains:
809 // newTVO - (oldTVO - dp)
810 // 2. Management fee released by re-amortization (positive when
811 // mfee decreased; zero when managementFeeRate == 0):
812 // oldMfee - newMfee
813 // 3. The overpayment's penalty interest part (= untrackedInterest
814 // for the overpayment path; see computeOverpaymentComponents):
815 // trackedInterestPart()
816 [[maybe_unused]] bool const fix320Enabled = rules.enabled(fixCleanup3_2_0);
817 XRPL_ASSERT_IF(
818 fix320Enabled,
819 overpaymentComponents.trackedPrincipalDelta ==
820 principalOutstandingProxy - newRoundedLoanState.principalOutstanding,
821 "xrpl::detail::doOverpayment : principal change agrees");
822
823 XRPL_ASSERT_IF(
824 fix320Enabled,
825 [&] {
826 Number const tvoChange = newRoundedLoanState.valueOutstanding -
827 (totalValueOutstandingProxy - overpaymentComponents.trackedPrincipalDelta);
828 Number const managementFeeReleased =
829 managementFeeOutstandingProxy - newRoundedLoanState.managementFeeDue;
830 Number const interestPart = overpaymentComponents.trackedInterestPart();
831 return loanPaymentParts.valueChange == tvoChange + managementFeeReleased + interestPart;
832 }(),
833 "xrpl::detail::doOverpayment : interest paid agrees");
834
835 XRPL_ASSERT_IF(
836 fix320Enabled,
837 overpaymentComponents.trackedPrincipalDelta == loanPaymentParts.principalPaid,
838 "xrpl::detail::doOverpayment : principal payment matches");
839
840 // All validations passed, so update the proxy objects (which will
841 // modify the actual Loan ledger object)
842 totalValueOutstandingProxy = newRoundedLoanState.valueOutstanding;
843 principalOutstandingProxy = newRoundedLoanState.principalOutstanding;
844 managementFeeOutstandingProxy = newRoundedLoanState.managementFeeDue;
845 periodicPaymentProxy = newLoanProperties.periodicPayment;
846
847 return loanPaymentParts;
848}
849
850/* Computes the payment components for a late payment.
851 *
852 * A late payment is made after the grace period has expired and includes:
853 * 1. All components of a regular periodic payment
854 * 2. Late payment penalty interest (accrued since the due date)
855 * 3. Late payment fee charged by the broker
856 *
857 * The late penalty interest increases the loan's total value (the borrower
858 * owes more than scheduled), while the regular payment components follow
859 * the normal amortization schedule.
860 *
861 * Implements equation (15) from XLS-66 spec, Section A-2 Equation Glossary
862 */
863std::expected<ExtendedPaymentComponents, TER>
865 Asset const& asset,
866 ApplyView const& view,
867 Number const& principalOutstanding,
868 std::int32_t nextDueDate,
869 ExtendedPaymentComponents const& periodic,
870 TenthBips32 lateInterestRate,
871 std::int32_t loanScale,
872 Number const& latePaymentFee,
873 STAmount const& amount,
874 TenthBips16 managementFeeRate,
876{
877 // Check if the due date has passed. If not, reject the payment as
878 // being too soon
879 if (!hasExpired(view, nextDueDate))
881
882 // Calculate the penalty interest based on how long the payment is overdue.
883 auto const latePaymentInterest = loanLatePaymentInterest(
884 principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate);
885
886 // Round the late interest and split it between the vault (net interest)
887 // and the broker (management fee portion). This lambda ensures we
888 // round before splitting to maintain precision.
889 auto const [roundedLateInterest, roundedLateManagementFee] = [&]() {
890 auto const interest = roundToAsset(asset, latePaymentInterest, loanScale);
891 return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
892 }();
893
894 XRPL_ASSERT(roundedLateInterest >= 0, "xrpl::detail::computeLatePayment : valid late interest");
895 XRPL_ASSERT_PARTS(
897 "xrpl::detail::computeLatePayment",
898 "no extra parts to this payment");
899
900 // Create the late payment components by copying the regular periodic
901 // payment and adding the late penalties. We use a lambda to construct
902 // this to keep the logic clear. This preserves all the other fields without
903 // having to enumerate them.
904
905 ExtendedPaymentComponents const late{
906 periodic,
907 // Untracked management fee includes:
908 // 1. Regular service fee (from periodic.untrackedManagementFee)
909 // 2. Late payment fee (fixed penalty)
910 // 3. Management fee portion of late interest
911 periodic.untrackedManagementFee + latePaymentFee + roundedLateManagementFee,
912
913 // Untracked interest includes:
914 // 1. Any untracked interest from the regular payment (usually 0)
915 // 2. Late penalty interest (increases loan value)
916 // This positive value indicates the loan's value increased due
917 // to the late payment.
918 periodic.untrackedInterest + roundedLateInterest};
919
920 XRPL_ASSERT_PARTS(
921 isRounded(asset, late.totalDue, loanScale),
922 "xrpl::detail::computeLatePayment",
923 "total due is rounded");
924
925 // Check that the borrower provided enough funds to cover the late payment.
926 // The late payment is more expensive than a regular payment due to the
927 // penalties.
928 if (amount < late.totalDue)
929 {
930 JLOG(j.warn()) << "Late loan payment amount is insufficient. Due: " << late.totalDue
931 << ", paid: " << amount;
933 }
934
935 return late;
936}
937
938/* Computes payment components for paying off a loan early (before final
939 * payment).
940 *
941 * A full payment closes the loan immediately, paying off all outstanding
942 * balances plus a prepayment penalty and any accrued interest since the last
943 * payment. This is different from the final scheduled payment, which has no
944 * prepayment penalty.
945 *
946 * The function calculates:
947 * - Accrued interest since last payment (time-based)
948 * - Prepayment penalty (percentage of remaining principal)
949 * - Close payment fee (fixed fee for early closure)
950 * - All remaining principal and outstanding fees
951 *
952 * The loan's value may increase or decrease depending on whether the prepayment
953 * penalty exceeds the scheduled interest that would have been paid.
954 *
955 * Implements equation (26) from XLS-66 spec, Section A-2 Equation Glossary
956 */
957std::expected<ExtendedPaymentComponents, TER>
959 Asset const& asset,
960 ApplyView& view,
961 Number const& principalOutstanding,
962 Number const& managementFeeOutstanding,
963 Number const& periodicPayment,
964 std::uint32_t paymentRemaining,
965 std::uint32_t prevPaymentDate,
966 std::uint32_t const startDate,
967 std::uint32_t const paymentInterval,
968 TenthBips32 const closeInterestRate,
969 std::int32_t loanScale,
970 Number const& totalInterestOutstanding,
971 Number const& periodicRate,
972 Number const& closePaymentFee,
973 STAmount const& amount,
974 TenthBips16 managementFeeRate,
976{
977 // Full payment must be made before the final scheduled payment.
978 if (paymentRemaining <= 1)
979 {
980 // If this is the last payment, it has to be a regular payment
981 JLOG(j.warn()) << "Last payment cannot be a full payment.";
983 }
984
985 // Calculate the theoretical principal based on the payment schedule.
986 // This theoretical (unrounded) value is used to compute interest and
987 // penalties accurately.
988 Number const theoreticalPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
989 view.rules(), periodicPayment, periodicRate, paymentRemaining);
990
991 // Full payment interest includes both accrued interest (time since last
992 // payment) and prepayment penalty (for closing early).
993 auto const fullPaymentInterest = computeFullPaymentInterest(
994 theoreticalPrincipalOutstanding,
995 periodicRate,
996 view.parentCloseTime(),
997 paymentInterval,
998 prevPaymentDate,
999 startDate,
1000 closeInterestRate);
1001
1002 // Split the full payment interest into net interest (to vault) and
1003 // management fee (to broker), applying proper rounding.
1004 auto const [roundedFullInterest, roundedFullManagementFee] = [&]() {
1005 auto const interest =
1006 roundToAsset(asset, fullPaymentInterest, loanScale, Number::RoundingMode::Downward);
1007 return computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
1008 }();
1009
1010 ExtendedPaymentComponents const full{
1012 // Pay off all tracked outstanding balances: principal, interest,
1013 // and fees.
1014 // This marks the loan as complete (final payment).
1015 .trackedValueDelta =
1016 principalOutstanding + totalInterestOutstanding + managementFeeOutstanding,
1017 .trackedPrincipalDelta = principalOutstanding,
1018
1019 // All outstanding management fees are paid. This zeroes out the
1020 // tracked fee balance.
1021 .trackedManagementFeeDelta = managementFeeOutstanding,
1022 .specialCase = PaymentSpecialCase::Final,
1023 },
1024
1025 // Untracked management fee includes:
1026 // 1. Close payment fee (fixed fee for early closure)
1027 // 2. Management fee on the full payment interest
1028 // 3. Minus the outstanding tracked fee (already accounted for above)
1029 // This can be negative because the outstanding fee is subtracted, but
1030 // it gets combined with trackedManagementFeeDelta in the final
1031 // accounting.
1032 closePaymentFee + roundedFullManagementFee - managementFeeOutstanding,
1033
1034 // Value change represents the difference between what the loan was
1035 // expected to earn (totalInterestOutstanding) and what it actually
1036 // earns (roundedFullInterest with prepayment penalty).
1037 // - Positive: Prepayment penalty exceeds scheduled interest (loan value
1038 // increases)
1039 // - Negative: Prepayment penalty is less than scheduled interest (loan
1040 // value decreases)
1041 roundedFullInterest - totalInterestOutstanding,
1042 };
1043
1044 XRPL_ASSERT_PARTS(
1045 isRounded(asset, full.totalDue, loanScale),
1046 "xrpl::detail::computeFullPayment",
1047 "total due is rounded");
1048
1049 JLOG(j.trace()) << "computeFullPayment result: periodicPayment: " << periodicPayment
1050 << ", periodicRate: " << periodicRate
1051 << ", paymentRemaining: " << paymentRemaining
1052 << ", theoreticalPrincipalOutstanding: " << theoreticalPrincipalOutstanding
1053 << ", fullPaymentInterest: " << fullPaymentInterest
1054 << ", roundedFullInterest: " << roundedFullInterest
1055 << ", roundedFullManagementFee: " << roundedFullManagementFee
1056 << ", untrackedInterest: " << full.untrackedInterest;
1057
1058 if (amount < full.totalDue)
1059 {
1060 // If the payment is less than the full payment amount, it's not
1061 // sufficient to be a full payment.
1063 }
1064
1065 return full;
1066}
1067
1068Number
1073
1074/* Computes the breakdown of a regular periodic payment into principal,
1075 * interest, and management fee components.
1076 *
1077 * This function determines how a single scheduled payment should be split among
1078 * the three tracked loan components. The calculation accounts for accumulated
1079 * rounding errors.
1080 *
1081 * The algorithm:
1082 * 1. Calculate what the loan state SHOULD be after this payment (target)
1083 * 2. Compare current state to target to get deltas
1084 * 3. Adjust deltas to handle rounding artifacts and edge cases
1085 * 4. Ensure deltas don't exceed available balances or payment amount
1086 *
1087 * Special handling for the final payment: all remaining balances are paid off
1088 * regardless of the periodic payment amount.
1089 *
1090 * Implements the pseudo-code function `compute_payment_due()`.
1091 */
1094 Rules const& rules,
1095 Asset const& asset,
1097 Number const& totalValueOutstanding,
1098 Number const& principalOutstanding,
1099 Number const& managementFeeOutstanding,
1100 Number const& periodicPayment,
1101 Number const& periodicRate,
1102 std::uint32_t paymentRemaining,
1103 TenthBips16 managementFeeRate)
1104{
1105 XRPL_ASSERT_PARTS(
1106 isRounded(asset, totalValueOutstanding, scale) &&
1107 isRounded(asset, principalOutstanding, scale) &&
1108 isRounded(asset, managementFeeOutstanding, scale),
1109 "xrpl::detail::computePaymentComponents",
1110 "Outstanding values are rounded");
1111 XRPL_ASSERT_PARTS(
1112 paymentRemaining > 0, "xrpl::detail::computePaymentComponents", "some payments remaining");
1113
1114 auto const roundedPeriodicPayment = roundPeriodicPayment(asset, periodicPayment, scale);
1115
1116 // Final payment: pay off everything remaining, ignoring the normal
1117 // periodic payment amount. This ensures the loan completes cleanly.
1118 if (paymentRemaining == 1 || totalValueOutstanding <= roundedPeriodicPayment)
1119 {
1120 // If there's only one payment left, we need to pay off each of the loan
1121 // parts.
1122 return PaymentComponents{
1123 .trackedValueDelta = totalValueOutstanding,
1124 .trackedPrincipalDelta = principalOutstanding,
1125 .trackedManagementFeeDelta = managementFeeOutstanding,
1126 .specialCase = PaymentSpecialCase::Final};
1127 }
1128
1129 // Calculate what the loan state SHOULD be after this payment (the target).
1130 // This is computed at full precision using the theoretical amortization.
1131 LoanState const trueTarget = computeTheoreticalLoanState(
1132 rules, periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate);
1133
1134 // Round the target to the loan's scale to match how actual loan values
1135 // are stored. With fixCleanup3_2_0 enabled, principal is rounded upward
1136 // and interest downward so that at coarse scale principal sticks at the
1137 // floor (until the final payment clears it) while interest absorbs each
1138 // periodic payment. Without the amendment the pre-existing round-to-
1139 // nearest behavior is preserved (which can hit the "Partial principal
1140 // payment" assertion on degenerate integer-scale loans).
1141 bool const fixCleanup320Enabled = rules.enabled(fixCleanup3_2_0);
1142 Number::RoundingMode const principalRounding =
1143 fixCleanup320Enabled ? Number::RoundingMode::Upward : Number::getround();
1144 Number::RoundingMode const interestRounding =
1145 fixCleanup320Enabled ? Number::RoundingMode::Downward : Number::getround();
1146 LoanState const roundedTarget = LoanState{
1147 .valueOutstanding = roundToAsset(asset, trueTarget.valueOutstanding, scale),
1148 .principalOutstanding =
1149 roundToAsset(asset, trueTarget.principalOutstanding, scale, principalRounding),
1150 .interestDue = roundToAsset(asset, trueTarget.interestDue, scale, interestRounding),
1151 .managementFeeDue = roundToAsset(asset, trueTarget.managementFeeDue, scale)};
1152
1153 // Get the current actual loan state from the ledger values
1154 LoanState const currentLedgerState =
1155 constructLoanState(totalValueOutstanding, principalOutstanding, managementFeeOutstanding);
1156
1157 // The difference between current and target states gives us the payment
1158 // components. Any discrepancies from accumulated rounding are captured
1159 // here.
1160
1161 LoanStateDeltas deltas = currentLedgerState - roundedTarget;
1162
1163 // Rounding can occasionally produce negative deltas. Zero them out.
1164 deltas.nonNegative();
1165
1166 XRPL_ASSERT_PARTS(
1167 deltas.principal <= currentLedgerState.principalOutstanding,
1168 "xrpl::detail::computePaymentComponents",
1169 "principal delta not greater than outstanding");
1170
1171 // Cap each component to never exceed what's actually outstanding
1172 deltas.principal = std::min(deltas.principal, currentLedgerState.principalOutstanding);
1173
1174 if (fixCleanup320Enabled)
1175 {
1176 XRPL_ASSERT_PARTS(
1177 deltas.interest <= currentLedgerState.interestDue,
1178 "xrpl::detail::computePaymentComponents",
1179 "interest due delta not greater than outstanding");
1180 }
1181 // Cap interest to both the outstanding amount AND what's left of the
1182 // periodic payment after principal is paid
1183 deltas.interest = std::min(
1184 {deltas.interest,
1185 std::max(kNumZero, roundedPeriodicPayment - deltas.principal),
1186 currentLedgerState.interestDue});
1187
1188 XRPL_ASSERT_PARTS(
1189 deltas.managementFee <= currentLedgerState.managementFeeDue,
1190 "xrpl::detail::computePaymentComponents",
1191 "management fee due delta not greater than outstanding");
1192
1193 // Cap management fee to both the outstanding amount AND what's left of the
1194 // periodic payment after principal and interest are paid
1195 deltas.managementFee = std::min(
1196 {deltas.managementFee,
1197 roundedPeriodicPayment - (deltas.principal + deltas.interest),
1198 currentLedgerState.managementFeeDue});
1199
1200 // The shortage must never be negative, which indicates that the parts are
1201 // trying to take more than the whole payment. The excess can be positive,
1202 // which indicates that we're not going to take the whole payment amount,
1203 // but if so, it must be small.
1204 auto takeFrom = [](Number& component, Number& excess) {
1205 if (excess > beast::kZero)
1206 {
1207 auto part = std::min(component, excess);
1208 component -= part;
1209 excess -= part;
1210 }
1211 XRPL_ASSERT_PARTS(
1212 excess >= beast::kZero,
1213 "xrpl::detail::computePaymentComponents",
1214 "excess non-negative");
1215 };
1216 // Helper to reduce deltas when they collectively exceed a limit.
1217 // Order matters: we prefer to reduce interest first (most flexible),
1218 // then management fee, then principal (least flexible).
1219 auto addressExcess = [&takeFrom](LoanStateDeltas& deltas, Number& excess) {
1220 // This order is based on where errors are the least problematic
1221 takeFrom(deltas.interest, excess);
1222 takeFrom(deltas.managementFee, excess);
1223 takeFrom(deltas.principal, excess);
1224 };
1225
1226 // Check if deltas exceed the total outstanding value. This should never
1227 // happen due to earlier caps, but handle it defensively.
1228 Number totalOverpayment = deltas.total() - currentLedgerState.valueOutstanding;
1229
1230 if (totalOverpayment > beast::kZero)
1231 {
1232 // LCOV_EXCL_START
1233 UNREACHABLE(
1234 "xrpl::detail::computePaymentComponents : payment exceeded loan "
1235 "state");
1236 addressExcess(deltas, totalOverpayment);
1237 // LCOV_EXCL_STOP
1238 }
1239
1240 // Check if deltas exceed the periodic payment amount. Reduce if needed.
1241 Number shortage = roundedPeriodicPayment - deltas.total();
1242
1243 XRPL_ASSERT_PARTS(
1244 isRounded(asset, shortage, scale),
1245 "xrpl::detail::computePaymentComponents",
1246 "shortage is rounded");
1247
1248 if (shortage < beast::kZero)
1249 {
1250 // Deltas exceed payment amount - reduce them proportionally
1251 Number excess = -shortage;
1252 addressExcess(deltas, excess);
1253 shortage = -excess;
1254 }
1255
1256 // At this point, shortage >= 0 means we're paying less than the full
1257 // periodic payment (due to rounding or component caps).
1258 // shortage < 0 would mean we're trying to pay more than allowed (bug).
1259 XRPL_ASSERT_PARTS(
1260 shortage >= beast::kZero,
1261 "xrpl::detail::computePaymentComponents",
1262 "no shortage or excess");
1263
1264 // Final validation that all components are valid
1265 XRPL_ASSERT_PARTS(
1266 deltas.total() == deltas.principal + deltas.interest + deltas.managementFee,
1267 "xrpl::detail::computePaymentComponents",
1268 "total value adds up");
1269
1270 XRPL_ASSERT_PARTS(
1271 deltas.principal >= beast::kZero &&
1272 deltas.principal <= currentLedgerState.principalOutstanding,
1273 "xrpl::detail::computePaymentComponents",
1274 "valid principal result");
1275 XRPL_ASSERT_PARTS(
1276 deltas.interest >= beast::kZero && deltas.interest <= currentLedgerState.interestDue,
1277 "xrpl::detail::computePaymentComponents",
1278 "valid interest result");
1279 XRPL_ASSERT_PARTS(
1280 deltas.managementFee >= beast::kZero &&
1281 deltas.managementFee <= currentLedgerState.managementFeeDue,
1282 "xrpl::detail::computePaymentComponents",
1283 "valid fee result");
1284
1285 XRPL_ASSERT_PARTS(
1286 deltas.principal + deltas.interest + deltas.managementFee > beast::kZero,
1287 "xrpl::detail::computePaymentComponents",
1288 "payment parts add to payment");
1289
1290 // Final safety clamp to ensure no value exceeds its outstanding balance
1291 return PaymentComponents{
1292 .trackedValueDelta =
1293 std::clamp(deltas.total(), kNumZero, currentLedgerState.valueOutstanding),
1294 .trackedPrincipalDelta =
1295 std::clamp(deltas.principal, kNumZero, currentLedgerState.principalOutstanding),
1296 .trackedManagementFeeDelta =
1297 std::clamp(deltas.managementFee, kNumZero, currentLedgerState.managementFeeDue),
1298 };
1299}
1300
1301/* Computes payment components for an overpayment scenario.
1302 *
1303 * An overpayment occurs when a borrower pays more than the scheduled periodic
1304 * payment amount. The overpayment is treated as extra principal reduction,
1305 * but incurs a fee and potentially a penalty interest charge.
1306 *
1307 * The calculation (Section 3.2.4.2.3 from XLS-66 spec):
1308 * 1. Calculate gross penalty interest on the overpayment amount
1309 * 2. Split the gross interest into net interest and management fee
1310 * 3. Calculate the penalty fee
1311 * 4. Determine the principal portion by subtracting the interest (gross) and
1312 * management fee from the overpayment amount
1313 *
1314 * Unlike regular payments which follow the amortization schedule, overpayments
1315 * apply to principal, reducing the loan balance and future interest costs.
1316 *
1317 * Equations (20), (21) and (22) from XLS-66 spec, Section A-2 Equation Glossary
1318 */
1319ExtendedPaymentComponents
1321 Rules const& rules,
1322 Asset const& asset,
1323 int32_t const loanScale,
1324 Number const& overpayment,
1325 TenthBips32 const overpaymentInterestRate,
1326 TenthBips32 const overpaymentFeeRate,
1327 TenthBips16 const managementFeeRate)
1328{
1329 XRPL_ASSERT_IF(
1330 rules.enabled(fixCleanup3_2_0),
1331 overpayment > 0 && isRounded(asset, overpayment, loanScale),
1332 "xrpl::detail::computeOverpaymentComponents : valid overpayment "
1333 "amount");
1334
1335 // First, deduct the fixed overpayment fee from the total amount.
1336 // This reduces the effective payment that will be applied to the loan.
1337 // Equation (22) from XLS-66 spec, Section A-2 Equation Glossary
1338 Number const overpaymentFee =
1339 roundToAsset(asset, tenthBipsOfValue(overpayment, overpaymentFeeRate), loanScale);
1340
1341 // Calculate the penalty interest on the effective payment amount.
1342 // This interest doesn't follow the normal amortization schedule - it's
1343 // a one-time charge for paying early.
1344 // Equation (20) and (21) from XLS-66 spec, Section A-2 Equation Glossary
1345 auto const [roundedOverpaymentInterest, roundedOverpaymentManagementFee] = [&]() {
1346 auto const interest =
1347 roundToAsset(asset, tenthBipsOfValue(overpayment, overpaymentInterestRate), loanScale);
1348 return detail::computeInterestAndFeeParts(asset, interest, managementFeeRate, loanScale);
1349 }();
1350
1351 auto const result = detail::ExtendedPaymentComponents{
1352 // Build the payment components, after fees and penalty
1353 // interest are deducted, the remainder goes entirely to principal
1354 // reduction.
1356 .trackedValueDelta = overpayment - overpaymentFee,
1357 .trackedPrincipalDelta = overpayment - roundedOverpaymentInterest -
1358 roundedOverpaymentManagementFee - overpaymentFee,
1359 .trackedManagementFeeDelta = roundedOverpaymentManagementFee,
1360 .specialCase = detail::PaymentSpecialCase::Extra},
1361 // Untracked management fee is the fixed overpayment fee
1362 overpaymentFee,
1363 // Untracked interest is the penalty interest charged for overpaying.
1364 // This is positive, representing a one-time cost, but it's typically
1365 // much smaller than the interest savings from reducing principal.
1366 // It is equal to the paymentComponents.trackedInterestPart()
1367 // but is kept separate for clarity.
1368 roundedOverpaymentInterest};
1369 XRPL_ASSERT_PARTS(
1370 result.trackedInterestPart() == roundedOverpaymentInterest,
1371 "xrpl::detail::computeOverpaymentComponents",
1372 "valid interest computation");
1373 return result;
1374}
1375
1376} // namespace detail
1377
1378detail::LoanStateDeltas
1379operator-(LoanState const& lhs, LoanState const& rhs)
1380{
1382 .principal = lhs.principalOutstanding - rhs.principalOutstanding,
1383 .interest = lhs.interestDue - rhs.interestDue,
1384 .managementFee = lhs.managementFeeDue - rhs.managementFeeDue,
1385 };
1386
1387 return result;
1388}
1389
1390LoanState
1392{
1393 LoanState result{
1394 .valueOutstanding = lhs.valueOutstanding - rhs.total(),
1395 .principalOutstanding = lhs.principalOutstanding - rhs.principal,
1396 .interestDue = lhs.interestDue - rhs.interest,
1397 .managementFeeDue = lhs.managementFeeDue - rhs.managementFee,
1398 };
1399
1400 return result;
1401}
1402
1403LoanState
1405{
1406 LoanState result{
1407 .valueOutstanding = lhs.valueOutstanding + rhs.total(),
1408 .principalOutstanding = lhs.principalOutstanding + rhs.principal,
1409 .interestDue = lhs.interestDue + rhs.interest,
1410 .managementFeeDue = lhs.managementFeeDue + rhs.managementFee,
1411 };
1412
1413 return result;
1414}
1415
1416TER
1418 Asset const& vaultAsset,
1419 Number const& principalRequested,
1420 bool expectInterest,
1421 std::uint32_t paymentTotal,
1422 LoanProperties const& properties,
1424{
1425 auto const totalInterestOutstanding =
1426 properties.loanState.valueOutstanding - principalRequested;
1427 // Guard 1: if there is no computed total interest over the life of the
1428 // loan for a non-zero interest rate, we cannot properly amortize the
1429 // loan
1430 if (expectInterest && totalInterestOutstanding <= 0)
1431 {
1432 // Unless this is a zero-interest loan, there must be some interest
1433 // due on the loan, even if it's (measurable) dust
1434 JLOG(j.warn()) << "Loan for " << principalRequested << " with interest has no interest due";
1435 return tecPRECISION_LOSS;
1436 }
1437 // Guard 1a: If there is any interest computed over the life of the
1438 // loan, for a zero interest rate, something went sideways.
1439 if (!expectInterest && totalInterestOutstanding > 0)
1440 {
1441 // LCOV_EXCL_START
1442 JLOG(j.warn()) << "Loan for " << principalRequested << " with no interest has interest due";
1443 return tecINTERNAL;
1444 // LCOV_EXCL_STOP
1445 }
1446
1447 // Guard 2: if the principal portion of the first periodic payment is
1448 // too small to be accurately represented with the given rounding mode,
1449 // raise an error
1450 if (properties.firstPaymentPrincipal <= 0)
1451 {
1452 // Check that some true (unrounded) principal is paid each period.
1453 // Since the first payment pays the least principal, if it's good,
1454 // they'll all be good. Note that the outstanding principal is
1455 // rounded, and may not change right away.
1456 JLOG(j.warn()) << "Loan is unable to pay principal.";
1457 return tecPRECISION_LOSS;
1458 }
1459
1460 // Guard 3: If the periodic payment is so small that it can't even be
1461 // rounded to a representable value, then the loan can't be paid. Also,
1462 // avoids dividing by 0.
1463 auto const roundedPayment =
1464 roundPeriodicPayment(vaultAsset, properties.periodicPayment, properties.loanScale);
1465 if (roundedPayment == beast::kZero)
1466 {
1467 JLOG(j.warn()) << "Loan Periodic payment (" << properties.periodicPayment
1468 << ") rounds to 0. ";
1469 return tecPRECISION_LOSS;
1470 }
1471
1472 // Guard 4: if the rounded periodic payment is large enough that the
1473 // loan can't be amortized in the specified number of payments, raise an
1474 // error
1475 {
1477
1478 if (std::int64_t const computedPayments{
1479 properties.loanState.valueOutstanding / roundedPayment};
1480 computedPayments != paymentTotal)
1481 {
1482 JLOG(j.warn()) << "Loan Periodic payment (" << properties.periodicPayment
1483 << ") rounding (" << roundedPayment << ") on a total value of "
1484 << properties.loanState.valueOutstanding
1485 << " can not complete the loan in the specified "
1486 "number of payments ("
1487 << computedPayments << " != " << paymentTotal << ")";
1488 return tecPRECISION_LOSS;
1489 }
1490 }
1491 return tesSUCCESS;
1492}
1493
1494/*
1495 * This function calculates the full payment interest accrued since the last
1496 * payment, plus any prepayment penalty.
1497 *
1498 * Equations (27) and (28) from XLS-66 spec, Section A-2 Equation Glossary
1499 */
1500Number
1502 Number const& theoreticalPrincipalOutstanding,
1503 Number const& periodicRate,
1504 NetClock::time_point parentCloseTime,
1505 std::uint32_t paymentInterval,
1506 std::uint32_t prevPaymentDate,
1507 std::uint32_t startDate,
1508 TenthBips32 closeInterestRate)
1509{
1510 auto const accruedInterest = detail::loanAccruedInterest(
1511 theoreticalPrincipalOutstanding,
1512 periodicRate,
1513 parentCloseTime,
1514 startDate,
1515 prevPaymentDate,
1516 paymentInterval);
1517 XRPL_ASSERT(
1518 accruedInterest >= 0,
1519 "xrpl::detail::computeFullPaymentInterest : valid accrued "
1520 "interest");
1521
1522 // Equation (28) from XLS-66 spec, Section A-2 Equation Glossary
1523 auto const prepaymentPenalty = closeInterestRate == beast::kZero
1524 ? Number{}
1525 : tenthBipsOfValue(theoreticalPrincipalOutstanding, closeInterestRate);
1526
1527 XRPL_ASSERT(
1528 prepaymentPenalty >= 0,
1529 "xrpl::detail::computeFullPaymentInterest : valid prepayment "
1530 "interest");
1531
1532 // Part of equation (27) from XLS-66 spec, Section A-2 Equation Glossary
1533 return accruedInterest + prepaymentPenalty;
1534}
1535
1536/* Calculates the theoretical loan state at maximum precision for a given point
1537 * in the amortization schedule.
1538 *
1539 * This function computes what the loan's outstanding balances should be based
1540 * on the periodic payment amount and number of payments remaining,
1541 * without considering any rounding that may have been applied to the actual
1542 * Loan object's state. This "theoretical" (unrounded) state is used as a target
1543 * for computing payment components and validating that the loan's tracked state
1544 * hasn't drifted too far from the theoretical values.
1545 *
1546 * The theoretical state serves several purposes:
1547 * 1. Computing the expected payment breakdown (principal, interest, fees)
1548 * 2. Detecting and correcting rounding errors that accumulate over time
1549 * 3. Validating that overpayments are calculated correctly
1550 * 4. Ensuring the loan will be fully paid off at the end of its term
1551 *
1552 * If paymentRemaining is 0, returns a fully zeroed-out LoanState,
1553 * representing a completely paid-off loan.
1554 *
1555 * Implements the `calculate_true_loan_state` function from the XLS-66 spec
1556 * section 3.2.4.4 Transaction Pseudo-code
1557 */
1558LoanState
1560 Rules const& rules,
1561 Number const& periodicPayment,
1562 Number const& periodicRate,
1563 std::uint32_t const paymentRemaining,
1564 TenthBips32 const managementFeeRate)
1565{
1566 if (paymentRemaining == 0)
1567 {
1568 return LoanState{
1569 .valueOutstanding = 0,
1570 .principalOutstanding = 0,
1571 .interestDue = 0,
1572 .managementFeeDue = 0};
1573 }
1574
1575 // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
1576 Number const totalValueOutstanding = periodicPayment * paymentRemaining;
1577
1578 Number const principalOutstanding = detail::loanPrincipalFromPeriodicPayment(
1579 rules, periodicPayment, periodicRate, paymentRemaining);
1580
1581 // Equation (31) from XLS-66 spec, Section A-2 Equation Glossary
1582 Number const interestOutstandingGross = totalValueOutstanding - principalOutstanding;
1583
1584 // Equation (32) from XLS-66 spec, Section A-2 Equation Glossary
1585 Number const managementFeeOutstanding =
1586 tenthBipsOfValue(interestOutstandingGross, managementFeeRate);
1587
1588 // Equation (33) from XLS-66 spec, Section A-2 Equation Glossary
1589 Number const interestOutstandingNet = interestOutstandingGross - managementFeeOutstanding;
1590
1591 return LoanState{
1592 .valueOutstanding = totalValueOutstanding,
1593 .principalOutstanding = principalOutstanding,
1594 .interestDue = interestOutstandingNet,
1595 .managementFeeDue = managementFeeOutstanding,
1596 };
1597};
1598
1599/* Constructs a LoanState from rounded Loan ledger object values.
1600 *
1601 * This function creates a LoanState structure from the three tracked values
1602 * stored in a Loan ledger object. Unlike calculateTheoreticalLoanState(), which
1603 * computes theoretical unrounded values, this function works with values
1604 * that have already been rounded to the loan's scale.
1605 *
1606 * The key difference from calculateTheoreticalLoanState():
1607 * - calculateTheoreticalLoanState: Computes theoretical values at full
1608 * precision
1609 * - constructRoundedLoanState: Builds state from actual rounded ledger values
1610 *
1611 * The interestDue field is derived from the other three values rather than
1612 * stored directly, since it can be calculated as:
1613 * interestDue = totalValueOutstanding - principalOutstanding -
1614 * managementFeeOutstanding
1615 *
1616 * This ensures consistency across the codebase and prevents copy-paste errors
1617 * when creating LoanState objects from Loan ledger data.
1618 */
1619LoanState
1621 Number const& totalValueOutstanding,
1622 Number const& principalOutstanding,
1623 Number const& managementFeeOutstanding)
1624{
1625 // This implementation is pretty trivial, but ensures the calculations
1626 // are consistent everywhere, and reduces copy/paste errors.
1627 return LoanState{
1628 .valueOutstanding = totalValueOutstanding,
1629 .principalOutstanding = principalOutstanding,
1630 .interestDue = totalValueOutstanding - principalOutstanding - managementFeeOutstanding,
1631 .managementFeeDue = managementFeeOutstanding};
1632}
1633
1634LoanState
1636{
1637 return constructLoanState(
1638 loan->at(sfTotalValueOutstanding),
1639 loan->at(sfPrincipalOutstanding),
1640 loan->at(sfManagementFeeOutstanding));
1641}
1642
1643/*
1644 * This function calculates the fee owed to the broker based on the asset,
1645 * value, and management fee rate.
1646 *
1647 * Equation (32) from XLS-66 spec, Section A-2 Equation Glossary
1648 */
1649Number
1651 Asset const& asset,
1652 Number const& value,
1653 TenthBips32 managementFeeRate,
1655{
1656 return roundToAsset(
1657 asset, tenthBipsOfValue(value, managementFeeRate), scale, Number::RoundingMode::Downward);
1658}
1659
1660/*
1661 * Given the loan parameters, compute the derived properties of the loan.
1662 *
1663 * Pulls together several formulas from the XLS-66 spec, which are noted at each
1664 * step, plus the concepts from 3.2.4.3 Conceptual Loan Value. They are used for
1665 * to check some of the conditions in 3.2.1.5 Failure Conditions for the LoanSet
1666 * transaction.
1667 */
1668LoanProperties
1670 Rules const& rules,
1671 Asset const& asset,
1672 Number const& principalOutstanding,
1673 TenthBips32 interestRate,
1674 std::uint32_t paymentInterval,
1675 std::uint32_t paymentsRemaining,
1676 TenthBips32 managementFeeRate,
1677 std::int32_t minimumScale)
1678{
1679 auto const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
1680 XRPL_ASSERT(interestRate == 0 || periodicRate > 0, "xrpl::computeLoanProperties : valid rate");
1681 return computeLoanProperties(
1682 rules,
1683 asset,
1684 principalOutstanding,
1685 periodicRate,
1686 paymentsRemaining,
1687 managementFeeRate,
1688 minimumScale);
1689}
1690
1691/*
1692 * Given the loan parameters, compute the derived properties of the loan.
1693 *
1694 * Pulls together several formulas from the XLS-66 spec, which are noted at each
1695 * step, plus the concepts from 3.2.4.3 Conceptual Loan Value. They are used for
1696 * to check some of the conditions in 3.2.1.5 Failure Conditions for the LoanSet
1697 * transaction.
1698 */
1699LoanProperties
1701 Rules const& rules,
1702 Asset const& asset,
1703 Number const& principalOutstanding,
1704 Number const& periodicRate,
1705 std::uint32_t paymentsRemaining,
1706 TenthBips32 managementFeeRate,
1707 std::int32_t minimumScale)
1708{
1709 auto const periodicPayment =
1710 detail::loanPeriodicPayment(rules, principalOutstanding, periodicRate, paymentsRemaining);
1711
1712 auto const [totalValueOutstanding, loanScale] = [&]() {
1713 // only round up if there should be interest
1714 NumberRoundModeGuard const mg(
1716 // Use STAmount's internal rounding instead of roundToAsset, because
1717 // we're going to use this result to determine the scale for all the
1718 // other rounding.
1719
1720 // Equation (30) from XLS-66 spec, Section A-2 Equation Glossary
1721 STAmount amount{asset, periodicPayment * paymentsRemaining};
1722
1723 // Base the loan scale on the total value, since that's going to be
1724 // the biggest number involved (barring unusual parameters for late,
1725 // full, or over payments)
1726 auto const loanScale = std::max(minimumScale, amount.exponent());
1727 XRPL_ASSERT_PARTS(
1728 (amount.integral() && loanScale == 0) ||
1729 (!amount.integral() && loanScale >= static_cast<Number>(amount).exponent()),
1730 "xrpl::computeLoanProperties",
1731 "loanScale value fits expectations");
1732
1733 // We may need to truncate the total value because of the minimum
1734 // scale
1735 amount = roundToAsset(asset, amount, loanScale);
1736
1737 return std::make_pair(amount, loanScale);
1738 }();
1739
1740 // Since we just figured out the loan scale, we haven't been able to
1741 // validate that the principal fits in it, so to allow this function to
1742 // succeed, round it here, and let the caller do the validation.
1743 auto const roundedPrincipalOutstanding =
1744 roundToAsset(asset, principalOutstanding, loanScale, Number::RoundingMode::ToNearest);
1745
1746 // Equation (31) from XLS-66 spec, Section A-2 Equation Glossary
1747 auto const totalInterestOutstanding = totalValueOutstanding - roundedPrincipalOutstanding;
1748 auto const feeOwedToBroker =
1749 computeManagementFee(asset, totalInterestOutstanding, managementFeeRate, loanScale);
1750
1751 // Compute the principal part of the first payment. This is needed
1752 // because the principal part may be rounded down to zero, which
1753 // would prevent the principal from ever being paid down.
1754 auto const firstPaymentPrincipal = [&]() {
1755 // Compute the parts for the first payment. Ensure that the
1756 // principal payment will actually change the principal.
1757 auto const startingState = computeTheoreticalLoanState(
1758 rules, periodicPayment, periodicRate, paymentsRemaining, managementFeeRate);
1759
1760 auto const firstPaymentState = computeTheoreticalLoanState(
1761 rules, periodicPayment, periodicRate, paymentsRemaining - 1, managementFeeRate);
1762
1763 // The unrounded principal part needs to be large enough to affect
1764 // the principal. What to do if not is left to the caller
1765 return startingState.principalOutstanding - firstPaymentState.principalOutstanding;
1766 }();
1767
1768 return LoanProperties{
1769 .periodicPayment = periodicPayment,
1770 .loanState =
1771 constructLoanState(totalValueOutstanding, roundedPrincipalOutstanding, feeOwedToBroker),
1772 .loanScale = loanScale,
1773 .firstPaymentPrincipal = firstPaymentPrincipal,
1774 };
1775}
1776
1777/*
1778 * This is the main function to make a loan payment.
1779 * This function handles regular, late, full, and overpayments.
1780 * It is an implementation of the make_payment function from the XLS-66
1781 * spec. Section 3.2.4.4
1782 */
1783std::expected<LoanPaymentParts, TER>
1785 Asset const& asset,
1786 ApplyView& view,
1787 SLE::ref loan,
1788 SLE::const_ref brokerSle,
1789 STAmount const& amount,
1790 LoanPaymentType const paymentType,
1792{
1793 using namespace Lending;
1794
1795 auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding);
1796 auto paymentRemainingProxy = loan->at(sfPaymentRemaining);
1797
1798 if (paymentRemainingProxy == 0 || principalOutstandingProxy == 0)
1799 {
1800 // Loan complete this is already checked in LoanPay::preclaim()
1801 // LCOV_EXCL_START
1802 JLOG(j.warn()) << "Loan is already paid off.";
1803 return std::unexpected(tecKILLED);
1804 // LCOV_EXCL_STOP
1805 }
1806
1807 auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding);
1808 auto managementFeeOutstandingProxy = loan->at(sfManagementFeeOutstanding);
1809
1810 // Next payment due date must be set unless the loan is complete
1811 auto nextDueDateProxy = loan->at(sfNextPaymentDueDate);
1812 if (*nextDueDateProxy == 0)
1813 {
1814 JLOG(j.warn()) << "Loan next payment due date is not set.";
1816 }
1817
1818 std::int32_t const loanScale = loan->at(sfLoanScale);
1819
1820 TenthBips32 const interestRate{loan->at(sfInterestRate)};
1821
1822 Number const serviceFee = loan->at(sfLoanServiceFee);
1823 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
1824
1825 Number const periodicPayment = loan->at(sfPeriodicPayment);
1826
1827 auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
1828 std::uint32_t const startDate = loan->at(sfStartDate);
1829
1830 std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);
1831
1832 // Compute the periodic rate that will be used for calculations
1833 // throughout
1834 Number const periodicRate = loanPeriodicRate(interestRate, paymentInterval);
1835 XRPL_ASSERT(interestRate == 0 || periodicRate > 0, "xrpl::loanMakePayment : valid rate");
1836
1837 XRPL_ASSERT(*totalValueOutstandingProxy > 0, "xrpl::loanMakePayment : valid total value");
1838
1839 view.update(loan);
1840
1841 // -------------------------------------------------------------
1842 // A late payment not flagged as late overrides all other options.
1843 if (paymentType != LoanPaymentType::Late && hasExpired(view, nextDueDateProxy))
1844 {
1845 // If the payment is late, and the late flag was not set, it's not
1846 // valid
1847 JLOG(j.warn()) << "Loan payment is overdue. Use the tfLoanLatePayment "
1848 "transaction "
1849 "flag to make a late payment. Loan was created on "
1850 << startDate << ", prev payment due date is " << prevPaymentDateProxy
1851 << ", next payment due date is " << nextDueDateProxy << ", ledger time is "
1852 << view.parentCloseTime().time_since_epoch().count();
1854 }
1855
1856 // -------------------------------------------------------------
1857 // full payment handling
1858 if (paymentType == LoanPaymentType::Full)
1859 {
1860 TenthBips32 const closeInterestRate{loan->at(sfCloseInterestRate)};
1861 Number const closePaymentFee = roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale);
1862
1863 LoanState const roundedLoanState = constructLoanState(
1864 totalValueOutstandingProxy, principalOutstandingProxy, managementFeeOutstandingProxy);
1865
1866 auto const fullPaymentComponents = detail::computeFullPayment(
1867 asset,
1868 view,
1869 principalOutstandingProxy,
1870 managementFeeOutstandingProxy,
1871 periodicPayment,
1872 paymentRemainingProxy,
1873 prevPaymentDateProxy,
1874 startDate,
1875 paymentInterval,
1876 closeInterestRate,
1877 loanScale,
1878 roundedLoanState.interestDue,
1879 periodicRate,
1880 closePaymentFee,
1881 amount,
1882 managementFeeRate,
1883 j);
1884
1885 if (fullPaymentComponents.has_value())
1886 {
1887 return doPayment(
1888 *fullPaymentComponents,
1889 totalValueOutstandingProxy,
1890 principalOutstandingProxy,
1891 managementFeeOutstandingProxy,
1892 paymentRemainingProxy,
1893 prevPaymentDateProxy,
1894 nextDueDateProxy,
1895 paymentInterval);
1896 }
1897
1898 if (fullPaymentComponents.error())
1899 {
1900 // error() will be the TER returned if a payment is not made. It
1901 // will only evaluate to true if it's unsuccessful. Otherwise,
1902 // tesSUCCESS means nothing was done, so continue.
1903 return std::unexpected(fullPaymentComponents.error());
1904 }
1905
1906 // LCOV_EXCL_START
1907 UNREACHABLE("xrpl::loanMakePayment : invalid full payment result");
1908 JLOG(j.error()) << "Full payment computation failed unexpectedly.";
1910 // LCOV_EXCL_STOP
1911 }
1912
1913 // -------------------------------------------------------------
1914 // compute the periodic payment info that will be needed whether the
1915 // payment is late or regular
1918 view.rules(),
1919 asset,
1920 loanScale,
1921 totalValueOutstandingProxy,
1922 principalOutstandingProxy,
1923 managementFeeOutstandingProxy,
1924 periodicPayment,
1925 periodicRate,
1926 paymentRemainingProxy,
1927 managementFeeRate),
1928 serviceFee};
1929 XRPL_ASSERT_PARTS(
1930 periodic.trackedPrincipalDelta >= 0,
1931 "xrpl::loanMakePayment",
1932 "regular payment valid principal");
1933
1934 // -------------------------------------------------------------
1935 // late payment handling
1936 if (paymentType == LoanPaymentType::Late)
1937 {
1938 TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
1939 Number const latePaymentFee = loan->at(sfLatePaymentFee);
1940
1941 auto const latePaymentComponents = detail::computeLatePayment(
1942 asset,
1943 view,
1944 principalOutstandingProxy,
1945 nextDueDateProxy,
1946 periodic,
1947 lateInterestRate,
1948 loanScale,
1949 latePaymentFee,
1950 amount,
1951 managementFeeRate,
1952 j);
1953
1954 if (latePaymentComponents.has_value())
1955 {
1956 return doPayment(
1957 *latePaymentComponents,
1958 totalValueOutstandingProxy,
1959 principalOutstandingProxy,
1960 managementFeeOutstandingProxy,
1961 paymentRemainingProxy,
1962 prevPaymentDateProxy,
1963 nextDueDateProxy,
1964 paymentInterval);
1965 }
1966
1967 if (latePaymentComponents.error())
1968 {
1969 // error() will be the TER returned if a payment is not made. It
1970 // will only evaluate to true if it's unsuccessful.
1971 return std::unexpected(latePaymentComponents.error());
1972 }
1973
1974 // LCOV_EXCL_START
1975 UNREACHABLE("xrpl::loanMakePayment : invalid late payment result");
1976 JLOG(j.error()) << "Late payment computation failed unexpectedly.";
1978 // LCOV_EXCL_STOP
1979 }
1980
1981 // -------------------------------------------------------------
1982 // regular periodic payment handling
1983
1984 XRPL_ASSERT_PARTS(
1985 paymentType == LoanPaymentType::Regular || paymentType == LoanPaymentType::Overpayment,
1986 "xrpl::loanMakePayment",
1987 "regular payment type");
1988
1989 // Keep a running total of the actual parts paid
1990 LoanPaymentParts totalParts;
1991 Number totalPaid;
1992 std::size_t numPayments = 0;
1993
1994 while ((amount >= (totalPaid + periodic.totalDue)) && paymentRemainingProxy > 0 &&
1995 numPayments < kLoanMaximumPaymentsPerTransaction)
1996 {
1997 // Try to make more payments
1998 XRPL_ASSERT_PARTS(
1999 periodic.trackedPrincipalDelta >= 0,
2000 "xrpl::loanMakePayment",
2001 "payment pays non-negative principal");
2002
2003 totalPaid += periodic.totalDue;
2004 totalParts += detail::doPayment(
2005 periodic,
2006 totalValueOutstandingProxy,
2007 principalOutstandingProxy,
2008 managementFeeOutstandingProxy,
2009 paymentRemainingProxy,
2010 prevPaymentDateProxy,
2011 nextDueDateProxy,
2012 paymentInterval);
2013 ++numPayments;
2014
2015 XRPL_ASSERT_PARTS(
2017 (paymentRemainingProxy == 0),
2018 "xrpl::loanMakePayment",
2019 "final payment is the final payment");
2020
2021 // Don't compute the next payment if this was the last payment
2023 break;
2024
2027 view.rules(),
2028 asset,
2029 loanScale,
2030 totalValueOutstandingProxy,
2031 principalOutstandingProxy,
2032 managementFeeOutstandingProxy,
2033 periodicPayment,
2034 periodicRate,
2035 paymentRemainingProxy,
2036 managementFeeRate),
2037 serviceFee};
2038 }
2039
2040 if (numPayments == 0)
2041 {
2042 JLOG(j.warn()) << "Regular loan payment amount is insufficient. Due: " << periodic.totalDue
2043 << ", paid: " << amount;
2045 }
2046
2047 XRPL_ASSERT_PARTS(
2048 totalParts.principalPaid + totalParts.interestPaid + totalParts.feePaid == totalPaid,
2049 "xrpl::loanMakePayment",
2050 "payment parts add up");
2051 XRPL_ASSERT_PARTS(totalParts.valueChange == 0, "xrpl::loanMakePayment", "no value change");
2052
2053 // -------------------------------------------------------------
2054 // overpayment handling
2055 //
2056 // If the "fixCleanup3_1_3" amendment is enabled, truncate "amount",
2057 // at the loan scale. If the raw value is used, the overpayment
2058 // amount could be meaningless dust. Trying to process such a small
2059 // amount will, at best, waste time when all the result values round
2060 // to zero. At worst, it can cause logical errors with tiny amounts
2061 // of interest that don't add up correctly.
2062 auto const roundedAmount = view.rules().enabled(fixCleanup3_1_3)
2063 ? roundToAsset(asset, amount, loanScale, Number::RoundingMode::TowardsZero)
2064 : amount;
2065 if (paymentType == LoanPaymentType::Overpayment && loan->isFlag(lsfLoanOverpayment) &&
2066 paymentRemainingProxy > 0 && totalPaid < roundedAmount &&
2067 numPayments < kLoanMaximumPaymentsPerTransaction)
2068 {
2069 TenthBips32 const overpaymentInterestRate{loan->at(sfOverpaymentInterestRate)};
2070 TenthBips32 const overpaymentFeeRate{loan->at(sfOverpaymentFee)};
2071
2072 // It shouldn't be possible for the overpayment to be greater than
2073 // totalValueOutstanding, because that would have been processed as
2074 // another normal payment. But cap it just in case.
2075 Number const overpaymentRaw =
2076 std::min(roundedAmount - totalPaid, *totalValueOutstandingProxy);
2077
2078 bool const fixEnabled = view.rules().enabled(fixCleanup3_2_0);
2079 Number const overpayment = fixEnabled
2080 ? roundToAsset(asset, overpaymentRaw, loanScale, Number::RoundingMode::Downward)
2081 : overpaymentRaw;
2082
2083 // Post-amendment, the rounded overpayment can be zero; pre-amendment
2084 // it's always positive given the surrounding guards.
2085 if (!fixEnabled || overpayment > 0)
2086 {
2087 detail::ExtendedPaymentComponents const overpaymentComponents =
2089 view.rules(),
2090 asset,
2091 loanScale,
2092 overpayment,
2093 overpaymentInterestRate,
2094 overpaymentFeeRate,
2095 managementFeeRate);
2096
2097 // Don't process an overpayment if the whole amount (or more!)
2098 // gets eaten by fees and interest.
2099 if (overpaymentComponents.trackedPrincipalDelta > 0)
2100 {
2101 XRPL_ASSERT_PARTS(
2102 overpaymentComponents.untrackedInterest >= beast::kZero,
2103 "xrpl::loanMakePayment",
2104 "overpayment penalty did not reduce value of loan");
2105 // Can't just use `periodicPayment` here, because it might
2106 // change
2107 auto periodicPaymentProxy = loan->at(sfPeriodicPayment);
2108 if (auto const overResult = detail::doOverpayment(
2109 view.rules(),
2110 asset,
2111 loanScale,
2112 overpaymentComponents,
2113 totalValueOutstandingProxy,
2114 principalOutstandingProxy,
2115 managementFeeOutstandingProxy,
2116 periodicPaymentProxy,
2117 periodicRate,
2118 paymentRemainingProxy,
2119 managementFeeRate,
2120 j))
2121 {
2122 totalParts += *overResult;
2123 }
2124 else if (overResult.error())
2125 {
2126 // error() will be the TER returned if a payment is not
2127 // made. It will only evaluate to true if it's unsuccessful.
2128 // Otherwise, tesSUCCESS means nothing was done, so
2129 // continue.
2130 return std::unexpected(overResult.error());
2131 }
2132 }
2133 }
2134 }
2135
2136 // Check the final results are rounded, to double-check that the
2137 // intermediate steps were rounded.
2138 XRPL_ASSERT(
2139 isRounded(asset, totalParts.principalPaid, loanScale) &&
2140 totalParts.principalPaid >= beast::kZero,
2141 "xrpl::loanMakePayment : total principal paid is valid");
2142 XRPL_ASSERT(
2143 isRounded(asset, totalParts.interestPaid, loanScale) &&
2144 totalParts.interestPaid >= beast::kZero,
2145 "xrpl::loanMakePayment : total interest paid is valid");
2146 XRPL_ASSERT(
2147 isRounded(asset, totalParts.valueChange, loanScale),
2148 "xrpl::loanMakePayment : loan value change is valid");
2149 XRPL_ASSERT(
2150 isRounded(asset, totalParts.feePaid, loanScale) && totalParts.feePaid >= beast::kZero,
2151 "xrpl::loanMakePayment : fee paid is valid");
2152 return totalParts;
2153}
2154} // namespace xrpl
T clamp(T... args)
A generic endpoint for log messages.
Definition Journal.h:38
Stream error() const
Definition Journal.h:315
Stream debug() const
Definition Journal.h:297
Stream trace() const
Severity stream access functions.
Definition Journal.h:291
Stream warn() const
Definition Journal.h:309
Writeable view to a ledger, for applying a transaction.
Definition ApplyView.h:118
virtual void update(SLE::ref sle)=0
Indicate changes to a peeked SLE.
std::chrono::time_point< NetClock > time_point
Definition chrono.h:46
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
static RoundingMode getround()
Definition Number.cpp:105
constexpr int exponent() const noexcept
Returns the exponent of the external view of the Number.
Definition Number.h:661
A view into a ledger.
Definition ReadView.h:31
virtual Rules const & rules() const =0
Returns the tx processing rules.
NetClock::time_point parentCloseTime() const
Returns the close time of the previous ledger.
Definition ReadView.h:90
Rules controlling protocol behavior.
Definition Rules.h:33
bool enabled(uint256 const &feature) const
Returns true if a feature is enabled.
Definition Rules.cpp:171
std::string getFullText() const override
Definition STAmount.cpp:636
bool integral() const noexcept
Definition STAmount.h:447
Asset const & asset() const
Definition STAmount.h:478
int exponent() const noexcept
Definition STAmount.h:441
bool isZeroAtScale(int scale) const
Checks if this amount evaluates to zero when constrained to a specific accounting scale.
std::shared_ptr< STLedgerEntry > const & ref
std::shared_ptr< STLedgerEntry const > const & const_ref
bool isFieldPresent(SField const &field) const
Definition STObject.cpp:454
T make_pair(T... args)
T max(T... args)
T min(T... args)
Number computePaymentFactor(Rules const &rules, Number const &periodicRate, std::uint32_t paymentsRemaining)
Number loanPrincipalFromPeriodicPayment(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentsRemaining)
Number computePowerMinusOneHybrid(Number const &periodicRate, std::uint32_t paymentsRemaining)
Number loanPeriodicPayment(Rules const &rules, Number const &principalOutstanding, Number const &periodicRate, std::uint32_t paymentsRemaining)
std::expected< ExtendedPaymentComponents, TER > computeLatePayment(Asset const &asset, ApplyView const &view, Number const &principalOutstanding, std::int32_t nextDueDate, ExtendedPaymentComponents const &periodic, TenthBips32 lateInterestRate, std::int32_t loanScale, Number const &latePaymentFee, STAmount const &amount, TenthBips16 managementFeeRate, beast::Journal j)
Number loanAccruedInterest(Number const &principalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t startDate, std::uint32_t prevPaymentDate, std::uint32_t paymentInterval)
std::pair< Number, Number > computeInterestAndFeeParts(Asset const &asset, Number const &interest, TenthBips16 managementFeeRate, std::int32_t loanScale)
Number loanLatePaymentInterest(Number const &principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, std::uint32_t nextPaymentDueDate)
std::expected< ExtendedPaymentComponents, TER > computeFullPayment(Asset const &asset, ApplyView &view, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, std::uint32_t paymentRemaining, std::uint32_t prevPaymentDate, std::uint32_t const startDate, std::uint32_t const paymentInterval, TenthBips32 const closeInterestRate, std::int32_t loanScale, Number const &totalInterestOutstanding, Number const &periodicRate, Number const &closePaymentFee, STAmount const &amount, TenthBips16 managementFeeRate, beast::Journal j)
std::expected< LoanPaymentParts, TER > doOverpayment(Rules const &rules, Asset const &asset, std::int32_t loanScale, ExtendedPaymentComponents const &overpaymentComponents, NumberProxy &totalValueOutstandingProxy, NumberProxy &principalOutstandingProxy, NumberProxy &managementFeeOutstandingProxy, NumberProxy &periodicPaymentProxy, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips16 const managementFeeRate, beast::Journal j)
std::expected< std::pair< LoanPaymentParts, LoanProperties >, TER > tryOverpayment(Rules const &rules, Asset const &asset, std::int32_t loanScale, ExtendedPaymentComponents const &overpaymentComponents, LoanState const &roundedLoanState, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentRemaining, TenthBips16 const managementFeeRate, beast::Journal j)
ExtendedPaymentComponents computeOverpaymentComponents(Rules const &rules, Asset const &asset, int32_t const loanScale, Number const &overpayment, TenthBips32 const overpaymentInterestRate, TenthBips32 const overpaymentFeeRate, TenthBips16 const managementFeeRate)
LoanPaymentParts doPayment(ExtendedPaymentComponents const &payment, NumberProxy &totalValueOutstandingProxy, NumberProxy &principalOutstandingProxy, NumberProxy &managementFeeOutstandingProxy, UInt32Proxy &paymentRemainingProxy, UInt32Proxy &prevPaymentDateProxy, UInt32OptionalProxy &nextDueDateProxy, std::uint32_t paymentInterval)
Number computePowerMinusOne(Number const &periodicRate, std::uint32_t paymentsRemaining)
PaymentComponents computePaymentComponents(Rules const &rules, Asset const &asset, std::int32_t scale, Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentRemaining, TenthBips16 managementFeeRate)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
static constexpr Number kNumZero
Definition Number.h:612
constexpr BaseUInt< Bits, Tag > operator+(BaseUInt< Bits, Tag > const &a, BaseUInt< Bits, Tag > const &b)
Definition base_uint.h:625
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
static auto sum(TCollection const &col)
Definition BookStep.cpp:993
TER canApplyToBrokerCover(ReadView const &view, SLE::const_ref sleBroker, Asset const &vaultAsset, STAmount const &amount, beast::Journal j, std::string_view logPrefix)
Broker cover preclaim precision guard (fixCleanup3_2_0).
Number operator-(Number const &x, Number const &y)
Definition Number.h:736
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:111
bool hasExpired(ReadView const &view, std::optional< std::uint32_t > const &exp)
Determines whether the given expiration time has passed.
Definition View.cpp:47
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:779
Number power(Number const &f, unsigned n)
Definition Number.cpp:1178
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:439
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:438
TER checkLoanGuards(Asset const &vaultAsset, Number const &principalRequested, bool expectInterest, std::uint32_t paymentTotal, LoanProperties const &properties, beast::Journal j)
std::expected< LoanPaymentParts, TER > loanMakePayment(Asset const &asset, ApplyView &view, SLE::ref loan, SLE::const_ref brokerSle, STAmount const &amount, LoanPaymentType const paymentType, beast::Journal j)
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:722
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
LoanState constructRoundedLoanState(SLE::const_ref loan)
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
Definition TER.h:634
@ tecINTERNAL
Definition TER.h:308
@ tecTOO_SOON
Definition TER.h:316
@ tecEXPIRED
Definition TER.h:312
@ tecPRECISION_LOSS
Definition TER.h:361
@ tecKILLED
Definition TER.h:314
@ tecINSUFFICIENT_PAYMENT
Definition TER.h:325
static constexpr std::uint32_t kSecondsInYear
LoanProperties computeLoanProperties(Rules const &rules, Asset const &asset, Number const &principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
@ tesSUCCESS
Definition TER.h:240
Number computeFullPaymentInterest(Number const &theoreticalPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)
bool checkLendingProtocolDependencies(Rules const &rules, STTx const &tx)
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
bool operator==(LoanPaymentParts const &other) const
LoanPaymentParts & operator+=(LoanPaymentParts const &other)
This structure captures the parts of a loan state.
Number principalOutstanding
T time_since_epoch(T... args)
T unexpected(T... args)