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