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