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