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