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