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