Skip to content

Commit 5773f24

Browse files
authored
Add additional documentation to Lending Protocol (#6037)
- documents core equations of the lending protocol
1 parent 08a6bef commit 5773f24

File tree

4 files changed

+682
-271
lines changed

4 files changed

+682
-271
lines changed

src/xrpld/app/misc/LendingHelpers.h

Lines changed: 190 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -27,22 +27,54 @@ roundPeriodicPayment(
2727
return roundToAsset(asset, periodicPayment, scale, Number::upward);
2828
}
2929

30-
/// This structure is explained in the XLS-66 spec, section 3.2.4.4 (Failure
31-
/// Conditions)
30+
/* Represents the breakdown of amounts to be paid and changes applied to the
31+
* Loan object while processing a loan payment.
32+
*
33+
* This structure is returned after processing a loan payment transaction and
34+
* captures the amounts that need to be paid. The actual ledger entry changes
35+
* are made in LoanPay based on this structure values.
36+
*
37+
* The sum of principalPaid, interestPaid, and feePaid represents the total
38+
* amount to be deducted from the borrower's account. The valueChange field
39+
* tracks whether the loan's total value increased or decreased beyond normal
40+
* amortization.
41+
*
42+
* This structure is explained in the XLS-66 spec, section 3.2.4.2 (Payment
43+
* Processing).
44+
*/
3245
struct LoanPaymentParts
3346
{
34-
/// principal_paid is the amount of principal that the payment covered.
47+
// The amount of principal paid that reduces the loan balance.
48+
// This amount is subtracted from sfPrincipalOutstanding in the Loan object
49+
// and paid to the Vault
3550
Number principalPaid = numZero;
36-
/// interest_paid is the amount of interest that the payment covered.
51+
52+
// The total amount of interest paid to the Vault.
53+
// This includes:
54+
// - Tracked interest from the amortization schedule
55+
// - Untracked interest (e.g., late payment penalty interest)
56+
// This value is always non-negative.
3757
Number interestPaid = numZero;
38-
/**
39-
* value_change is the amount by which the total value of the Loan changed.
40-
* If value_change < 0, Loan value decreased.
41-
* If value_change > 0, Loan value increased.
42-
* This is 0 for regular payments.
43-
*/
58+
59+
// The change in the loan's total value outstanding.
60+
// - If valueChange < 0: Loan value decreased
61+
// - If valueChange > 0: Loan value increased
62+
// - If valueChange = 0: No value adjustment
63+
//
64+
// For regular on-time payments, this is always 0. Non-zero values occur
65+
// when:
66+
// - Overpayments reduce the loan balance beyond the scheduled amount
67+
// - Late payments add penalty interest to the loan value
68+
// - Early full payment may increase or decrease the loan value based on
69+
// terms
4470
Number valueChange = numZero;
45-
/// feePaid is amount of fee that is paid to the broker
71+
72+
/* The total amount of fees paid to the Broker.
73+
* This includes:
74+
* - Tracked management fees from the amortization schedule
75+
* - Untracked fees (e.g., late payment fees, service fees, origination
76+
* fees) This value is always non-negative.
77+
*/
4678
Number feePaid = numZero;
4779

4880
LoanPaymentParts&
@@ -52,46 +84,71 @@ struct LoanPaymentParts
5284
operator==(LoanPaymentParts const& other) const;
5385
};
5486

55-
/** This structure describes the initial "computed" properties of a loan.
87+
/* Describes the initial computed properties of a loan.
5688
*
57-
* It is used at loan creation and when the terms of a loan change, such as
58-
* after an overpayment.
89+
* This structure contains the fundamental calculated values that define a
90+
* loan's payment structure and amortization schedule. These properties are
91+
* computed:
92+
* - At loan creation (LoanSet transaction)
93+
* - When loan terms change (e.g., after an overpayment that reduces the loan
94+
* balance)
5995
*/
6096
struct LoanProperties
6197
{
98+
// The unrounded amount to be paid at each regular payment period.
99+
// Calculated using the standard amortization formula based on principal,
100+
// interest rate, and number of payments.
101+
// The actual amount paid in the LoanPay transaction must be rounded up to
102+
// the precision of the asset and loan.
62103
Number periodicPayment;
104+
105+
// The total amount the borrower will pay over the life of the loan.
106+
// Equal to periodicPayment * paymentsRemaining.
107+
// This includes principal, interest, and management fees.
63108
Number totalValueOutstanding;
109+
110+
// The total management fee that will be paid to the broker over the
111+
// loan's lifetime. This is a percentage of the total interest (gross)
112+
// as specified by the broker's management fee rate.
64113
Number managementFeeOwedToBroker;
114+
115+
// The scale (decimal places) used for rounding all loan amounts.
116+
// This is the maximum of:
117+
// - The asset's native scale
118+
// - A minimum scale required to represent the periodic payment accurately
119+
// All loan state values (principal, interest, fees) are rounded to this
120+
// scale.
65121
std::int32_t loanScale;
122+
123+
// The principal portion of the first payment.
66124
Number firstPaymentPrincipal;
67125
};
68126

69-
/** This structure captures the current state of a loan and all the
70-
relevant parts.
71-
72-
Whether the values are raw (unrounded) or rounded will
73-
depend on how it was computed.
74-
75-
Many of the fields can be derived from each other, but they're all provided
76-
here to reduce code duplication and possible mistakes.
77-
e.g.
78-
* interestOutstanding = valueOutstanding - principalOutstanding
79-
* interestDue = interestOutstanding - managementFeeDue
127+
/** This structure captures the parts of a loan state.
128+
*
129+
* Whether the values are raw (unrounded) or rounded will depend on how it was
130+
* computed.
131+
*
132+
* Many of the fields can be derived from each other, but they're all provided
133+
* here to reduce code duplication and possible mistakes.
134+
* e.g.
135+
* * interestOutstanding = valueOutstanding - principalOutstanding
136+
* * interestDue = interestOutstanding - managementFeeDue
80137
*/
81138
struct LoanState
82139
{
83-
/// Total value still due to be paid by the borrower.
140+
// Total value still due to be paid by the borrower.
84141
Number valueOutstanding;
85-
/// Prinicipal still due to be paid by the borrower.
142+
// Principal still due to be paid by the borrower.
86143
Number principalOutstanding;
87-
/// Interest still due to be paid TO the Vault.
144+
// Interest still due to be paid to the Vault.
88145
// This is a portion of interestOutstanding
89146
Number interestDue;
90-
/// Management fee still due to be paid TO the broker.
147+
// Management fee still due to be paid to the broker.
91148
// This is a portion of interestOutstanding
92149
Number managementFeeDue;
93150

94-
/// Interest still due to be paid by the borrower.
151+
// Interest still due to be paid by the borrower.
95152
Number
96153
interestOutstanding() const
97154
{
@@ -199,42 +256,133 @@ namespace detail {
199256

200257
enum class PaymentSpecialCase { none, final, extra };
201258

202-
/// This structure is used internally to compute the breakdown of a
203-
/// single loan payment
259+
/* Represents a single loan payment component parts.
260+
261+
* This structure captures the "delta" (change) values that will be applied to
262+
* the tracked fields in the Loan ledger object when a payment is processed.
263+
*
264+
* These are called "deltas" because they represent the amount by which each
265+
* corresponding field in the Loan object will be reduced.
266+
* They are "tracked" as they change tracked loan values.
267+
*/
204268
struct PaymentComponents
205269
{
206-
// tracked values are rounded to the asset and loan scale, and correspond to
207-
// fields in the Loan ledger object.
208-
// trackedValueDelta modifies sfTotalValueOutstanding.
270+
// The change in total value outstanding for this payment.
271+
// This amount will be subtracted from sfTotalValueOutstanding in the Loan
272+
// object. Equal to the sum of trackedPrincipalDelta,
273+
// trackedInterestPart(), and trackedManagementFeeDelta.
209274
Number trackedValueDelta;
210-
// trackedPrincipalDelta modifies sfPrincipalOutstanding.
275+
276+
// The change in principal outstanding for this payment.
277+
// This amount will be subtracted from sfPrincipalOutstanding in the Loan
278+
// object, representing the portion of the payment that reduces the
279+
// original loan amount.
211280
Number trackedPrincipalDelta;
212-
// trackedManagementFeeDelta modifies sfManagementFeeOutstanding. It will
213-
// not include any "extra" fees that go directly to the broker, such as late
214-
// fees.
281+
282+
// The change in management fee outstanding for this payment.
283+
// This amount will be subtracted from sfManagementFeeOutstanding in the
284+
// Loan object. This represents only the tracked management fees from the
285+
// amortization schedule and does not include additional untracked fees
286+
// (such as late payment fees) that go directly to the broker.
215287
Number trackedManagementFeeDelta;
216288

289+
// Indicates if this payment has special handling requirements.
290+
// - none: Regular scheduled payment
291+
// - final: The last payment that closes out the loan
292+
// - extra: An additional payment beyond the regular schedule (overpayment)
217293
PaymentSpecialCase specialCase = PaymentSpecialCase::none;
218294

295+
// Calculates the tracked interest portion of this payment.
296+
// This is derived from the other components as:
297+
// trackedValueDelta - trackedPrincipalDelta - trackedManagementFeeDelta
298+
//
299+
// @return The amount of tracked interest included in this payment that
300+
// will be paid to the vault.
219301
Number
220302
trackedInterestPart() const;
221303
};
222304

223-
// This structure describes the difference between two LoanState objects so that
224-
// the differences between components don't have to be tracked individually,
225-
// risking more errors. How that difference is used depends on the context.
305+
/* Extends PaymentComponents with untracked payment amounts.
306+
*
307+
* This structure adds untracked fees and interest to the base
308+
* PaymentComponents, representing amounts that don't affect the Loan object's
309+
* tracked state but are still part of the total payment due from the borrower.
310+
*
311+
* Untracked amounts include:
312+
* - Late payment fees that go directly to the Broker
313+
* - Late payment penalty interest that goes directly to the Vault
314+
* - Service fees
315+
*
316+
* The key distinction is that tracked amounts reduce the Loan object's state
317+
* (sfTotalValueOutstanding, sfPrincipalOutstanding,
318+
* sfManagementFeeOutstanding), while untracked amounts are paid directly to the
319+
* recipient without affecting the loan's amortization schedule.
320+
*/
321+
struct ExtendedPaymentComponents : public PaymentComponents
322+
{
323+
// Additional management fees that go directly to the Broker.
324+
// This includes fees not part of the standard amortization schedule
325+
// (e.g., late fees, service fees, origination fees).
326+
// This value may be negative, though the final value returned in
327+
// LoanPaymentParts.feePaid will never be negative.
328+
Number untrackedManagementFee;
329+
330+
// Additional interest that goes directly to the Vault.
331+
// This includes interest not part of the standard amortization schedule
332+
// (e.g., late payment penalty interest).
333+
// This value may be negative, though the final value returned in
334+
// LoanPaymentParts.interestPaid will never be negative.
335+
Number untrackedInterest;
336+
337+
// The complete amount due from the borrower for this payment.
338+
// Calculated as: trackedValueDelta + untrackedInterest +
339+
// untrackedManagementFee
340+
//
341+
// This value is used to validate that the payment amount provided by the
342+
// borrower is sufficient to cover all components of the payment.
343+
Number totalDue;
344+
345+
ExtendedPaymentComponents(
346+
PaymentComponents const& p,
347+
Number fee,
348+
Number interest = numZero)
349+
: PaymentComponents(p)
350+
, untrackedManagementFee(fee)
351+
, untrackedInterest(interest)
352+
, totalDue(
353+
trackedValueDelta + untrackedInterest + untrackedManagementFee)
354+
{
355+
}
356+
};
357+
358+
/* Represents the differences between two loan states.
359+
*
360+
* This structure is used to capture the change in each component of a loan's
361+
* state, typically when computing the difference between two LoanState objects
362+
* (e.g., before and after a payment). It is a convenient way to capture changes
363+
* in each component. How that difference is used depends on the context.
364+
*/
226365
struct LoanStateDeltas
227366
{
367+
// The difference in principal outstanding between two loan states.
228368
Number principal;
369+
370+
// The difference in interest due between two loan states.
229371
Number interest;
372+
373+
// The difference in management fee outstanding between two loan states.
230374
Number managementFee;
231375

376+
/* Calculates the total change across all components.
377+
* @return The sum of principal, interest, and management fee deltas.
378+
*/
232379
Number
233380
total() const
234381
{
235382
return principal + interest + managementFee;
236383
}
237384

385+
// Ensures all delta values are non-negative.
238386
void
239387
nonNegative();
240388
};

0 commit comments

Comments
 (0)