rippled
Loading...
Searching...
No Matches
LendingHelpers_test.cpp
1#include <xrpl/beast/unit_test/suite.h>
2// DO NOT REMOVE
3#include <test/jtx.h>
4#include <test/jtx/Account.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/mpt.h>
7
8#include <xrpl/beast/xor_shift_engine.h>
9#include <xrpl/protocol/SField.h>
10#include <xrpl/server/LoadFeeTrack.h>
11#include <xrpl/tx/transactors/lending/LendingHelpers.h>
12#include <xrpl/tx/transactors/lending/LoanSet.h>
13#include <xrpl/tx/transactors/system/Batch.h>
14
15#include <string>
16#include <vector>
17
18namespace xrpl {
19namespace test {
20
22{
23 void
25 {
26 using namespace jtx;
27 using namespace xrpl::detail;
28 struct TestCase
29 {
30 std::string name;
31 Number periodicRate;
32 std::uint32_t paymentsRemaining;
33 Number expectedRaisedRate;
34 };
35
36 auto const testCases = std::vector<TestCase>{
37 {
38 .name = "Zero payments remaining",
39 .periodicRate = Number{5, -2},
40 .paymentsRemaining = 0,
41 .expectedRaisedRate = Number{1}, // (1 + r)^0 = 1
42 },
43 {
44 .name = "One payment remaining",
45 .periodicRate = Number{5, -2},
46 .paymentsRemaining = 1,
47 .expectedRaisedRate = Number{105, -2},
48 }, // 1.05^1
49 {
50 .name = "Multiple payments remaining",
51 .periodicRate = Number{5, -2},
52 .paymentsRemaining = 3,
53 .expectedRaisedRate = Number{1157625, -6},
54 }, // 1.05^3
55 {
56 .name = "Zero periodic rate",
57 .periodicRate = Number{0},
58 .paymentsRemaining = 5,
59 .expectedRaisedRate = Number{1}, // (1 + 0)^5 = 1
60 }};
61
62 for (auto const& tc : testCases)
63 {
64 testcase("computeRaisedRate: " + tc.name);
65
66 auto const computedRaisedRate =
67 computeRaisedRate(tc.periodicRate, tc.paymentsRemaining);
68 BEAST_EXPECTS(
69 computedRaisedRate == tc.expectedRaisedRate,
70 "Raised rate mismatch: expected " + to_string(tc.expectedRaisedRate) + ", got " +
71 to_string(computedRaisedRate));
72 }
73 }
74
75 void
77 {
78 using namespace jtx;
79 using namespace xrpl::detail;
80 struct TestCase
81 {
82 std::string name;
83 Number periodicRate;
84 std::uint32_t paymentsRemaining;
85 Number expectedPaymentFactor;
86 };
87
88 auto const testCases = std::vector<TestCase>{
89 {
90 .name = "Zero periodic rate",
91 .periodicRate = Number{0},
92 .paymentsRemaining = 4,
93 .expectedPaymentFactor = Number{25, -2},
94 }, // 1/4 = 0.25
95 {
96 .name = "One payment remaining",
97 .periodicRate = Number{5, -2},
98 .paymentsRemaining = 1,
99 .expectedPaymentFactor = Number{105, -2},
100 }, // 0.05/1 = 1.05
101 {
102 .name = "Multiple payments remaining",
103 .periodicRate = Number{5, -2},
104 .paymentsRemaining = 3,
105 .expectedPaymentFactor = Number{3672085646312450436, -19},
106 }, // from calc
107 {
108 .name = "Zero payments remaining",
109 .periodicRate = Number{5, -2},
110 .paymentsRemaining = 0,
111 .expectedPaymentFactor = Number{0},
112 } // edge case
113 };
114
115 for (auto const& tc : testCases)
116 {
117 testcase("computePaymentFactor: " + tc.name);
118
119 auto const computedPaymentFactor =
120 computePaymentFactor(tc.periodicRate, tc.paymentsRemaining);
121 BEAST_EXPECTS(
122 computedPaymentFactor == tc.expectedPaymentFactor,
123 "Payment factor mismatch: expected " + to_string(tc.expectedPaymentFactor) +
124 ", got " + to_string(computedPaymentFactor));
125 }
126 }
127
128 void
130 {
131 using namespace jtx;
132 using namespace xrpl::detail;
133
134 struct TestCase
135 {
136 std::string name;
137 Number principalOutstanding;
138 Number periodicRate;
139 std::uint32_t paymentsRemaining;
140 Number expectedPeriodicPayment;
141 };
142
143 auto const testCases = std::vector<TestCase>{
144 {
145 .name = "Zero principal outstanding",
146 .principalOutstanding = Number{0},
147 .periodicRate = Number{5, -2},
148 .paymentsRemaining = 5,
149 .expectedPeriodicPayment = Number{0},
150 },
151 {
152 .name = "Zero payments remaining",
153 .principalOutstanding = Number{1'000},
154 .periodicRate = Number{5, -2},
155 .paymentsRemaining = 0,
156 .expectedPeriodicPayment = Number{0},
157 },
158 {
159 .name = "Zero periodic rate",
160 .principalOutstanding = Number{1'000},
161 .periodicRate = Number{0},
162 .paymentsRemaining = 4,
163 .expectedPeriodicPayment = Number{250},
164 },
165 {
166 .name = "Standard case",
167 .principalOutstanding = Number{1'000},
168 .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
169 .paymentsRemaining = 3,
170 .expectedPeriodicPayment = Number{389569066396123265, -15}, // from calc
171 },
172 };
173
174 for (auto const& tc : testCases)
175 {
176 testcase("loanPeriodicPayment: " + tc.name);
177
178 auto const computedPeriodicPayment =
179 loanPeriodicPayment(tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
180 BEAST_EXPECTS(
181 computedPeriodicPayment == tc.expectedPeriodicPayment,
182 "Periodic payment mismatch: expected " + to_string(tc.expectedPeriodicPayment) +
183 ", got " + to_string(computedPeriodicPayment));
184 }
185 }
186
187 void
189 {
190 using namespace jtx;
191 using namespace xrpl::detail;
192
193 struct TestCase
194 {
195 std::string name;
196 Number periodicPayment;
197 Number periodicRate;
198 std::uint32_t paymentsRemaining;
199 Number expectedPrincipalOutstanding;
200 };
201
202 auto const testCases = std::vector<TestCase>{
203 {
204 .name = "Zero periodic payment",
205 .periodicPayment = Number{0},
206 .periodicRate = Number{5, -2},
207 .paymentsRemaining = 5,
208 .expectedPrincipalOutstanding = Number{0},
209 },
210 {
211 .name = "Zero payments remaining",
212 .periodicPayment = Number{1'000},
213 .periodicRate = Number{5, -2},
214 .paymentsRemaining = 0,
215 .expectedPrincipalOutstanding = Number{0},
216 },
217 {
218 .name = "Zero periodic rate",
219 .periodicPayment = Number{250},
220 .periodicRate = Number{0},
221 .paymentsRemaining = 4,
222 .expectedPrincipalOutstanding = Number{1'000},
223 },
224 {
225 .name = "Standard case",
226 .periodicPayment = Number{389569066396123265, -15}, // from calc
227 .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
228 .paymentsRemaining = 3,
229 .expectedPrincipalOutstanding = Number{1'000},
230 },
231 };
232
233 for (auto const& tc : testCases)
234 {
235 testcase("loanPrincipalFromPeriodicPayment: " + tc.name);
236
237 auto const computedPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
238 tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
239 BEAST_EXPECTS(
240 computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
241 "Principal outstanding mismatch: expected " +
242 to_string(tc.expectedPrincipalOutstanding) + ", got " +
243 to_string(computedPrincipalOutstanding));
244 }
245 }
246
247 void
249 {
250 testcase("computeOverpaymentComponents");
251 using namespace jtx;
252 using namespace xrpl::detail;
253
254 Account const issuer{"issuer"};
255 PrettyAsset const IOU = issuer["IOU"];
256 int32_t const loanScale = 1;
257 auto const overpayment = Number{1'000};
258 auto const overpaymentInterestRate = TenthBips32{10'000}; // 10%
259 auto const overpaymentFeeRate = TenthBips32{50'000}; // 50%
260 auto const managementFeeRate = TenthBips16{10'000}; // 10%
261
262 auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000
263 auto const expectedOverpaymentInterestGross = Number{100}; // 10% of 1,000
264 auto const expectedOverpaymentInterestNet = Number{90}; // 100 - 10% of 100
265 auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
266 auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
267
268 auto const components = detail::computeOverpaymentComponents(
269 IOU,
270 loanScale,
272 overpaymentInterestRate,
273 overpaymentFeeRate,
274 managementFeeRate);
275
276 BEAST_EXPECT(components.untrackedManagementFee == expectedOverpaymentFee);
277
278 BEAST_EXPECT(components.untrackedInterest == expectedOverpaymentInterestNet);
279
280 BEAST_EXPECT(components.trackedInterestPart() == expectedOverpaymentInterestNet);
281
282 BEAST_EXPECT(components.trackedManagementFeeDelta == expectedOverpaymentManagementFee);
283 BEAST_EXPECT(components.trackedPrincipalDelta == expectedPrincipalPortion);
284 BEAST_EXPECT(
285 components.trackedManagementFeeDelta + components.untrackedInterest ==
286 expectedOverpaymentInterestGross);
287
288 BEAST_EXPECT(
289 components.trackedManagementFeeDelta + components.untrackedInterest +
290 components.trackedPrincipalDelta + components.untrackedManagementFee ==
292 }
293
294 void
296 {
297 using namespace jtx;
298 using namespace xrpl::detail;
299
300 struct TestCase
301 {
302 std::string name;
303 Number interest;
304 TenthBips16 managementFeeRate;
305 Number expectedInterestPart;
306 Number expectedFeePart;
307 };
308
309 Account const issuer{"issuer"};
310 PrettyAsset const IOU = issuer["IOU"];
311 std::int32_t const loanScale = 1;
312
313 auto const testCases = std::vector<TestCase>{
314 {.name = "Zero interest",
315 .interest = Number{0},
316 .managementFeeRate = TenthBips16{10'000},
317 .expectedInterestPart = Number{0},
318 .expectedFeePart = Number{0}},
319 {.name = "Zero fee rate",
320 .interest = Number{1'000},
321 .managementFeeRate = TenthBips16{0},
322 .expectedInterestPart = Number{1'000},
323 .expectedFeePart = Number{0}},
324 {.name = "10% fee rate",
325 .interest = Number{1'000},
326 .managementFeeRate = TenthBips16{10'000},
327 .expectedInterestPart = Number{900},
328 .expectedFeePart = Number{100}},
329 };
330
331 for (auto const& tc : testCases)
332 {
333 testcase("computeInterestAndFeeParts: " + tc.name);
334
335 auto const [computedInterestPart, computedFeePart] =
336 computeInterestAndFeeParts(IOU, tc.interest, tc.managementFeeRate, loanScale);
337 BEAST_EXPECTS(
338 computedInterestPart == tc.expectedInterestPart,
339 "Interest part mismatch: expected " + to_string(tc.expectedInterestPart) +
340 ", got " + to_string(computedInterestPart));
341 BEAST_EXPECTS(
342 computedFeePart == tc.expectedFeePart,
343 "Fee part mismatch: expected " + to_string(tc.expectedFeePart) + ", got " +
344 to_string(computedFeePart));
345 }
346 }
347
348 void
350 {
351 using namespace jtx;
352 using namespace xrpl::detail;
353 struct TestCase
354 {
355 std::string name;
356 Number principalOutstanding;
357 TenthBips32 lateInterestRate;
358 NetClock::time_point parentCloseTime;
359 std::uint32_t nextPaymentDueDate;
360 Number expectedLateInterest;
361 };
362
363 auto const testCases = std::vector<TestCase>{
364 {
365 .name = "On-time payment",
366 .principalOutstanding = Number{1'000},
367 .lateInterestRate = TenthBips32{10'000}, // 10%
368 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
369 .nextPaymentDueDate = 3'000,
370 .expectedLateInterest = Number{0},
371 },
372 {
373 .name = "Early payment",
374 .principalOutstanding = Number{1'000},
375 .lateInterestRate = TenthBips32{10'000}, // 10%
376 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
377 .nextPaymentDueDate = 4'000,
378 .expectedLateInterest = Number{0},
379 },
380 {
381 .name = "No principal outstanding",
382 .principalOutstanding = Number{0},
383 .lateInterestRate = TenthBips32{10'000}, // 10%
384 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
385 .nextPaymentDueDate = 2'000,
386 .expectedLateInterest = Number{0},
387 },
388 {
389 .name = "No late interest rate",
390 .principalOutstanding = Number{1'000},
391 .lateInterestRate = TenthBips32{0}, // 0%
392 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
393 .nextPaymentDueDate = 2'000,
394 .expectedLateInterest = Number{0},
395 },
396 {
397 .name = "Late payment",
398 .principalOutstanding = Number{1'000},
399 .lateInterestRate = TenthBips32{100'000}, // 100%
400 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
401 .nextPaymentDueDate = 2'000,
402 .expectedLateInterest = Number{317097919837645865, -19}, // from calc
403 },
404 };
405
406 for (auto const& tc : testCases)
407 {
408 testcase("loanLatePaymentInterest: " + tc.name);
409
410 auto const computedLateInterest = loanLatePaymentInterest(
411 tc.principalOutstanding,
412 tc.lateInterestRate,
413 tc.parentCloseTime,
414 tc.nextPaymentDueDate);
415 BEAST_EXPECTS(
416 computedLateInterest == tc.expectedLateInterest,
417 "Late interest mismatch: expected " + to_string(tc.expectedLateInterest) +
418 ", got " + to_string(computedLateInterest));
419 }
420 }
421
422 void
424 {
425 using namespace jtx;
426 using namespace xrpl::detail;
427 struct TestCase
428 {
429 std::string name;
430 Number principalOutstanding;
431 Number periodicRate;
432 NetClock::time_point parentCloseTime;
433 std::uint32_t startDate;
434 std::uint32_t prevPaymentDate;
435 std::uint32_t paymentInterval;
436 Number expectedAccruedInterest;
437 };
438
439 auto const testCases = std::vector<TestCase>{
440 {
441 .name = "Zero principal outstanding",
442 .principalOutstanding = Number{0},
443 .periodicRate = Number{5, -2},
444 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
445 .startDate = 2'000,
446 .prevPaymentDate = 2'500,
447 .paymentInterval = 30 * 24 * 60 * 60,
448 .expectedAccruedInterest = Number{0},
449 },
450 {
451 .name = "Before start date",
452 .principalOutstanding = Number{1'000},
453 .periodicRate = Number{5, -2},
454 .parentCloseTime = NetClock::time_point{NetClock::duration{1'000}},
455 .startDate = 2'000,
456 .prevPaymentDate = 1'500,
457 .paymentInterval = 30 * 24 * 60 * 60,
458 .expectedAccruedInterest = Number{0},
459 },
460 {
461 .name = "Zero periodic rate",
462 .principalOutstanding = Number{1'000},
463 .periodicRate = Number{0},
464 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
465 .startDate = 2'000,
466 .prevPaymentDate = 2'500,
467 .paymentInterval = 30 * 24 * 60 * 60,
468 .expectedAccruedInterest = Number{0},
469 },
470 {
471 .name = "Zero payment interval",
472 .principalOutstanding = Number{1'000},
473 .periodicRate = Number{5, -2},
474 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
475 .startDate = 2'000,
476 .prevPaymentDate = 2'500,
477 .paymentInterval = 0,
478 .expectedAccruedInterest = Number{0},
479 },
480 {
481 .name = "Standard case",
482 .principalOutstanding = Number{1'000},
483 .periodicRate = Number{5, -2},
484 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
485 .startDate = 1'000,
486 .prevPaymentDate = 2'000,
487 .paymentInterval = 30 * 24 * 60 * 60,
488 .expectedAccruedInterest = Number{1929012345679012346, -20}, // from calc
489 },
490 };
491
492 for (auto const& tc : testCases)
493 {
494 testcase("loanAccruedInterest: " + tc.name);
495
496 auto const computedAccruedInterest = loanAccruedInterest(
497 tc.principalOutstanding,
498 tc.periodicRate,
499 tc.parentCloseTime,
500 tc.startDate,
501 tc.prevPaymentDate,
502 tc.paymentInterval);
503 BEAST_EXPECTS(
504 computedAccruedInterest == tc.expectedAccruedInterest,
505 "Accrued interest mismatch: expected " + to_string(tc.expectedAccruedInterest) +
506 ", got " + to_string(computedAccruedInterest));
507 }
508 }
509
510 // This test overlaps with testLoanAccruedInterest, the test cases only
511 // exercise the computeFullPaymentInterest parts unique to it.
512 void
514 {
515 using namespace jtx;
516 using namespace xrpl::detail;
517
518 struct TestCase
519 {
520 std::string name;
521 Number rawPrincipalOutstanding;
522 Number periodicRate;
523 NetClock::time_point parentCloseTime;
524 std::uint32_t paymentInterval;
525 std::uint32_t prevPaymentDate;
526 std::uint32_t startDate;
527 TenthBips32 closeInterestRate;
528 Number expectedFullPaymentInterest;
529 };
530
531 auto const testCases = std::vector<TestCase>{
532 {
533 .name = "Zero principal outstanding",
534 .rawPrincipalOutstanding = Number{0},
535 .periodicRate = Number{5, -2},
536 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
537 .paymentInterval = 30 * 24 * 60 * 60,
538 .prevPaymentDate = 2'000,
539 .startDate = 1'000,
540 .closeInterestRate = TenthBips32{10'000},
541 .expectedFullPaymentInterest = Number{0},
542 },
543 {
544 .name = "Zero close interest rate",
545 .rawPrincipalOutstanding = Number{1'000},
546 .periodicRate = Number{5, -2},
547 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
548 .paymentInterval = 30 * 24 * 60 * 60,
549 .prevPaymentDate = 2'000,
550 .startDate = 1'000,
551 .closeInterestRate = TenthBips32{0},
552 .expectedFullPaymentInterest = Number{1929012345679012346, -20}, // from calc
553 },
554 {
555 .name = "Standard case",
556 .rawPrincipalOutstanding = Number{1'000},
557 .periodicRate = Number{5, -2},
558 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
559 .paymentInterval = 30 * 24 * 60 * 60,
560 .prevPaymentDate = 2'000,
561 .startDate = 1'000,
562 .closeInterestRate = TenthBips32{10'000},
563 .expectedFullPaymentInterest = Number{1000192901234567901, -16}, // from calc
564 },
565 };
566
567 for (auto const& tc : testCases)
568 {
569 testcase("computeFullPaymentInterest: " + tc.name);
570
571 auto const computedFullPaymentInterest = computeFullPaymentInterest(
572 tc.rawPrincipalOutstanding,
573 tc.periodicRate,
574 tc.parentCloseTime,
575 tc.paymentInterval,
576 tc.prevPaymentDate,
577 tc.startDate,
578 tc.closeInterestRate);
579 BEAST_EXPECTS(
580 computedFullPaymentInterest == tc.expectedFullPaymentInterest,
581 "Full payment interest mismatch: expected " +
582 to_string(tc.expectedFullPaymentInterest) + ", got " +
583 to_string(computedFullPaymentInterest));
584 }
585 }
586
587 void
589 {
590 // This test ensures that overpayment with no interest works correctly.
591 testcase("tryOverpayment - No Interest No Fee");
592
593 using namespace jtx;
594 using namespace xrpl::detail;
595
596 Env const env{*this};
597 Account const issuer{"issuer"};
598 PrettyAsset const asset = issuer["USD"];
599 std::int32_t const loanScale = -5;
600 TenthBips16 const managementFeeRate{0}; // 0%
601 TenthBips32 const loanInterestRate{0}; // 0%
602 Number const loanPrincipal{1'000};
603 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
604 std::uint32_t const paymentsRemaining = 10;
605 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
606 Number const overpaymentAmount{50};
607
608 auto const overpaymentComponents = computeOverpaymentComponents(
609 asset, loanScale, overpaymentAmount, TenthBips32(0), TenthBips32(0), managementFeeRate);
610
611 auto const loanProperties = computeLoanProperties(
612 asset,
613 loanPrincipal,
614 loanInterestRate,
615 paymentInterval,
616 paymentsRemaining,
617 managementFeeRate,
618 loanScale);
619
620 auto const ret = tryOverpayment(
621 asset,
622 loanScale,
623 overpaymentComponents,
624 loanProperties.loanState,
625 loanProperties.periodicPayment,
626 periodicRate,
627 paymentsRemaining,
628 managementFeeRate,
629 env.journal);
630
631 BEAST_EXPECT(ret);
632
633 auto const& [actualPaymentParts, newLoanProperties] = *ret;
634 auto const& newState = newLoanProperties.loanState;
635
636 // =========== VALIDATE PAYMENT PARTS ===========
637 BEAST_EXPECTS(
638 actualPaymentParts.valueChange == 0,
639 " valueChange mismatch: expected 0, got " + to_string(actualPaymentParts.valueChange));
640
641 BEAST_EXPECTS(
642 actualPaymentParts.feePaid == 0,
643 " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid));
644
645 BEAST_EXPECTS(
646 actualPaymentParts.interestPaid == 0,
647 " interestPaid mismatch: expected 0, got " +
648 to_string(actualPaymentParts.interestPaid));
649
650 BEAST_EXPECTS(
651 actualPaymentParts.principalPaid == overpaymentAmount,
652 " principalPaid mismatch: expected " + to_string(overpaymentAmount) + ", got " +
653 to_string(actualPaymentParts.principalPaid));
654
655 // =========== VALIDATE STATE CHANGES ===========
656 BEAST_EXPECTS(
657 loanProperties.loanState.interestDue - newState.interestDue == 0,
658 " interest change mismatch: expected 0, got " +
659 to_string(loanProperties.loanState.interestDue - newState.interestDue));
660
661 BEAST_EXPECTS(
662 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
663 " management fee change mismatch: expected 0, got " +
664 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
665
666 BEAST_EXPECTS(
667 actualPaymentParts.principalPaid ==
668 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
669 " principalPaid mismatch: expected " +
670 to_string(
671 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
672 ", got " + to_string(actualPaymentParts.principalPaid));
673 }
674
675 void
677 {
678 testcase("tryOverpayment - No Interest With Overpayment Fee");
679
680 using namespace jtx;
681 using namespace xrpl::detail;
682
683 Env const env{*this};
684 Account const issuer{"issuer"};
685 PrettyAsset const asset = issuer["USD"];
686 std::int32_t const loanScale = -5;
687 TenthBips16 const managementFeeRate{0}; // 0%
688 TenthBips32 const loanInterestRate{0}; // 0%
689 Number const loanPrincipal{1'000};
690 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
691 std::uint32_t const paymentsRemaining = 10;
692 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
693
694 auto const overpaymentComponents = computeOverpaymentComponents(
695 asset,
696 loanScale,
697 Number{50, 0},
698 TenthBips32(0),
699 TenthBips32(10'000), // 10% overpayment fee
700 managementFeeRate);
701
702 auto const loanProperties = computeLoanProperties(
703 asset,
704 loanPrincipal,
705 loanInterestRate,
706 paymentInterval,
707 paymentsRemaining,
708 managementFeeRate,
709 loanScale);
710
711 auto const ret = tryOverpayment(
712 asset,
713 loanScale,
714 overpaymentComponents,
715 loanProperties.loanState,
716 loanProperties.periodicPayment,
717 periodicRate,
718 paymentsRemaining,
719 managementFeeRate,
720 env.journal);
721
722 BEAST_EXPECT(ret);
723
724 auto const& [actualPaymentParts, newLoanProperties] = *ret;
725 auto const& newState = newLoanProperties.loanState;
726
727 // =========== VALIDATE PAYMENT PARTS ===========
728 BEAST_EXPECTS(
729 actualPaymentParts.valueChange == 0,
730 " valueChange mismatch: expected 0, got " + to_string(actualPaymentParts.valueChange));
731
732 BEAST_EXPECTS(
733 actualPaymentParts.feePaid == 5,
734 " feePaid mismatch: expected 5, got " + to_string(actualPaymentParts.feePaid));
735
736 BEAST_EXPECTS(
737 actualPaymentParts.principalPaid == 45,
738 " principalPaid mismatch: expected 45, got `" +
739 to_string(actualPaymentParts.principalPaid));
740
741 BEAST_EXPECTS(
742 actualPaymentParts.interestPaid == 0,
743 " interestPaid mismatch: expected 0, got " +
744 to_string(actualPaymentParts.interestPaid));
745
746 // =========== VALIDATE STATE CHANGES ===========
747 // With no Loan interest, interest outstanding should not change
748 BEAST_EXPECTS(
749 loanProperties.loanState.interestDue - newState.interestDue == 0,
750 " interest change mismatch: expected 0, got " +
751 to_string(loanProperties.loanState.interestDue - newState.interestDue));
752
753 // With no Loan management fee, management fee due should not change
754 BEAST_EXPECTS(
755 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
756 " management fee change mismatch: expected 0, got " +
757 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
758
759 BEAST_EXPECTS(
760 actualPaymentParts.principalPaid ==
761 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
762 " principalPaid mismatch: expected " +
763 to_string(
764 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
765 ", got " + to_string(actualPaymentParts.principalPaid));
766 }
767
768 void
770 {
771 testcase("tryOverpayment - Loan Interest, No Overpayment Fees");
772
773 using namespace jtx;
774 using namespace xrpl::detail;
775
776 Env const env{*this};
777 Account const issuer{"issuer"};
778 PrettyAsset const asset = issuer["USD"];
779 std::int32_t const loanScale = -5;
780 TenthBips16 const managementFeeRate{0}; // 0%
781 TenthBips32 const loanInterestRate{10'000}; // 10%
782 Number const loanPrincipal{1'000};
783 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
784 std::uint32_t const paymentsRemaining = 10;
785 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
786
787 auto const overpaymentComponents = computeOverpaymentComponents(
788 asset,
789 loanScale,
790 Number{50, 0},
791 TenthBips32(0), // no overpayment interest
792 TenthBips32(0), // 0% overpayment fee
793 managementFeeRate);
794
795 auto const loanProperties = computeLoanProperties(
796 asset,
797 loanPrincipal,
798 loanInterestRate,
799 paymentInterval,
800 paymentsRemaining,
801 managementFeeRate,
802 loanScale);
803
804 auto const ret = tryOverpayment(
805 asset,
806 loanScale,
807 overpaymentComponents,
808 loanProperties.loanState,
809 loanProperties.periodicPayment,
810 periodicRate,
811 paymentsRemaining,
812 managementFeeRate,
813 env.journal);
814
815 BEAST_EXPECT(ret);
816
817 auto const& [actualPaymentParts, newLoanProperties] = *ret;
818 auto const& newState = newLoanProperties.loanState;
819
820 // =========== VALIDATE PAYMENT PARTS ===========
821 // with no overpayment interest portion, value change should equal
822 // interest decrease
823 BEAST_EXPECTS(
824 (actualPaymentParts.valueChange == Number{-228802, -5}),
825 " valueChange mismatch: expected " + to_string(Number{-228802, -5}) + ", got " +
826 to_string(actualPaymentParts.valueChange));
827
828 // with no fee portion, fee paid should be zero
829 BEAST_EXPECTS(
830 actualPaymentParts.feePaid == 0,
831 " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid));
832
833 BEAST_EXPECTS(
834 actualPaymentParts.principalPaid == 50,
835 " principalPaid mismatch: expected 50, got `" +
836 to_string(actualPaymentParts.principalPaid));
837
838 // with no interest portion, interest paid should be zero
839 BEAST_EXPECTS(
840 actualPaymentParts.interestPaid == 0,
841 " interestPaid mismatch: expected 0, got " +
842 to_string(actualPaymentParts.interestPaid));
843
844 // =========== VALIDATE STATE CHANGES ===========
845 BEAST_EXPECTS(
846 actualPaymentParts.principalPaid ==
847 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
848 " principalPaid mismatch: expected " +
849 to_string(
850 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
851 ", got " + to_string(actualPaymentParts.principalPaid));
852
853 BEAST_EXPECTS(
854 actualPaymentParts.valueChange ==
855 newState.interestDue - loanProperties.loanState.interestDue,
856 " valueChange mismatch: expected " +
857 to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
858 to_string(actualPaymentParts.valueChange));
859
860 // With no Loan management fee, management fee due should not change
861 BEAST_EXPECTS(
862 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
863 " management fee change mismatch: expected 0, got " +
864 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
865 }
866
867 void
869 {
870 testcase("tryOverpayment - Loan Interest, Overpayment Interest, No Fee");
871
872 using namespace jtx;
873 using namespace xrpl::detail;
874
875 Env const env{*this};
876 Account const issuer{"issuer"};
877 PrettyAsset const asset = issuer["USD"];
878 std::int32_t const loanScale = -5;
879 TenthBips16 const managementFeeRate{0}; // 0%
880 TenthBips32 const loanInterestRate{10'000}; // 10%
881 Number const loanPrincipal{1'000};
882 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
883 std::uint32_t const paymentsRemaining = 10;
884 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
885
886 auto const overpaymentComponents = computeOverpaymentComponents(
887 asset,
888 loanScale,
889 Number{50, 0},
890 TenthBips32(10'000), // 10% overpayment interest
891 TenthBips32(0), // 0% overpayment fee
892 managementFeeRate);
893
894 auto const loanProperties = computeLoanProperties(
895 asset,
896 loanPrincipal,
897 loanInterestRate,
898 paymentInterval,
899 paymentsRemaining,
900 managementFeeRate,
901 loanScale);
902
903 auto const ret = tryOverpayment(
904 asset,
905 loanScale,
906 overpaymentComponents,
907 loanProperties.loanState,
908 loanProperties.periodicPayment,
909 periodicRate,
910 paymentsRemaining,
911 managementFeeRate,
912 env.journal);
913
914 BEAST_EXPECT(ret);
915
916 auto const& [actualPaymentParts, newLoanProperties] = *ret;
917 auto const& newState = newLoanProperties.loanState;
918
919 // =========== VALIDATE PAYMENT PARTS ===========
920 // with overpayment interest portion, interest paid should be 5
921 BEAST_EXPECTS(
922 actualPaymentParts.interestPaid == 5,
923 " interestPaid mismatch: expected 5, got " +
924 to_string(actualPaymentParts.interestPaid));
925
926 // With overpayment interest portion, value change should equal the
927 // interest decrease plus overpayment interest portion
928 BEAST_EXPECTS(
929 (actualPaymentParts.valueChange ==
930 Number{-205922, -5} + actualPaymentParts.interestPaid),
931 " valueChange mismatch: expected " +
932 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid) +
933 ", got " + to_string(actualPaymentParts.valueChange));
934
935 // with no fee portion, fee paid should be zero
936 BEAST_EXPECTS(
937 actualPaymentParts.feePaid == 0,
938 " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid));
939
940 BEAST_EXPECTS(
941 actualPaymentParts.principalPaid == 45,
942 " principalPaid mismatch: expected 45, got `" +
943 to_string(actualPaymentParts.principalPaid));
944
945 // =========== VALIDATE STATE CHANGES ===========
946 BEAST_EXPECTS(
947 actualPaymentParts.principalPaid ==
948 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
949 " principalPaid mismatch: expected " +
950 to_string(
951 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
952 ", got " + to_string(actualPaymentParts.principalPaid));
953
954 // The change in interest is equal to the value change sans the
955 // overpayment interest
956 BEAST_EXPECTS(
957 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
958 newState.interestDue - loanProperties.loanState.interestDue,
959 " valueChange mismatch: expected " +
960 to_string(
961 newState.interestDue - loanProperties.loanState.interestDue +
962 actualPaymentParts.interestPaid) +
963 ", got " + to_string(actualPaymentParts.valueChange));
964
965 // With no Loan management fee, management fee due should not change
966 BEAST_EXPECTS(
967 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
968 " management fee change mismatch: expected 0, got " +
969 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
970 }
971
972 void
974 {
975 testcase(
976 "tryOverpayment - Loan Interest and Fee, Overpayment Interest, No "
977 "Fee");
978
979 using namespace jtx;
980 using namespace xrpl::detail;
981
982 Env const env{*this};
983 Account const issuer{"issuer"};
984 PrettyAsset const asset = issuer["USD"];
985 std::int32_t const loanScale = -5;
986 TenthBips16 const managementFeeRate{10'000}; // 10%
987 TenthBips32 const loanInterestRate{10'000}; // 10%
988 Number const loanPrincipal{1'000};
989 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
990 std::uint32_t const paymentsRemaining = 10;
991 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
992
993 auto const overpaymentComponents = computeOverpaymentComponents(
994 asset,
995 loanScale,
996 Number{50, 0},
997 TenthBips32(10'000), // 10% overpayment interest
998 TenthBips32(0), // 0% overpayment fee
999 managementFeeRate);
1000
1001 auto const loanProperties = computeLoanProperties(
1002 asset,
1003 loanPrincipal,
1004 loanInterestRate,
1005 paymentInterval,
1006 paymentsRemaining,
1007 managementFeeRate,
1008 loanScale);
1009
1010 auto const ret = tryOverpayment(
1011 asset,
1012 loanScale,
1013 overpaymentComponents,
1014 loanProperties.loanState,
1015 loanProperties.periodicPayment,
1016 periodicRate,
1017 paymentsRemaining,
1018 managementFeeRate,
1019 env.journal);
1020
1021 BEAST_EXPECT(ret);
1022
1023 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1024 auto const& newState = newLoanProperties.loanState;
1025
1026 // =========== VALIDATE PAYMENT PARTS ===========
1027
1028 // Since there is loan management fee, the fee is charged against
1029 // overpayment interest portion first, so interest paid remains 4.5
1030 BEAST_EXPECTS(
1031 (actualPaymentParts.interestPaid == Number{45, -1}),
1032 " interestPaid mismatch: expected 4.5, got " +
1033 to_string(actualPaymentParts.interestPaid));
1034
1035 // With overpayment interest portion, value change should equal the
1036 // interest decrease plus overpayment interest portion
1037 BEAST_EXPECTS(
1038 (actualPaymentParts.valueChange ==
1039 Number{-18533, -4} + actualPaymentParts.interestPaid),
1040 " valueChange mismatch: expected " +
1041 to_string(Number{-18533, -4} + actualPaymentParts.interestPaid) + ", got " +
1042 to_string(actualPaymentParts.valueChange));
1043
1044 // While there is no overpayment fee, fee paid should equal the
1045 // management fee charged against the overpayment interest portion
1046 BEAST_EXPECTS(
1047 (actualPaymentParts.feePaid == Number{5, -1}),
1048 " feePaid mismatch: expected 0.5, got " + to_string(actualPaymentParts.feePaid));
1049
1050 BEAST_EXPECTS(
1051 actualPaymentParts.principalPaid == 45,
1052 " principalPaid mismatch: expected 45, got `" +
1053 to_string(actualPaymentParts.principalPaid));
1054
1055 // =========== VALIDATE STATE CHANGES ===========
1056 BEAST_EXPECTS(
1057 actualPaymentParts.principalPaid ==
1058 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1059 " principalPaid mismatch: expected " +
1060 to_string(
1061 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1062 ", got " + to_string(actualPaymentParts.principalPaid));
1063
1064 // Note that the management fee value change is not captured, as this
1065 // value is not needed to correctly update the Vault state.
1066 BEAST_EXPECTS(
1067 (newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
1068 Number{-20592, -5}),
1069 " management fee change mismatch: expected " + to_string(Number{-20592, -5}) +
1070 ", got " +
1071 to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
1072
1073 BEAST_EXPECTS(
1074 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1075 newState.interestDue - loanProperties.loanState.interestDue,
1076 " valueChange mismatch: expected " +
1077 to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
1078 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
1079 }
1080
1081 void
1083 {
1084 testcase("tryOverpayment - Loan Interest, Fee, Overpayment Interest, Fee");
1085
1086 using namespace jtx;
1087 using namespace xrpl::detail;
1088
1089 Env const env{*this};
1090 Account const issuer{"issuer"};
1091 PrettyAsset const asset = issuer["USD"];
1092 std::int32_t const loanScale = -5;
1093 TenthBips16 const managementFeeRate{10'000}; // 10%
1094 TenthBips32 const loanInterestRate{10'000}; // 10%
1095 Number const loanPrincipal{1'000};
1096 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
1097 std::uint32_t const paymentsRemaining = 10;
1098 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
1099
1100 auto const overpaymentComponents = computeOverpaymentComponents(
1101 asset,
1102 loanScale,
1103 Number{50, 0},
1104 TenthBips32(10'000), // 10% overpayment interest
1105 TenthBips32(10'000), // 10% overpayment fee
1106 managementFeeRate);
1107
1108 auto const loanProperties = computeLoanProperties(
1109 asset,
1110 loanPrincipal,
1111 loanInterestRate,
1112 paymentInterval,
1113 paymentsRemaining,
1114 managementFeeRate,
1115 loanScale);
1116
1117 auto const ret = tryOverpayment(
1118 asset,
1119 loanScale,
1120 overpaymentComponents,
1121 loanProperties.loanState,
1122 loanProperties.periodicPayment,
1123 periodicRate,
1124 paymentsRemaining,
1125 managementFeeRate,
1126 env.journal);
1127
1128 BEAST_EXPECT(ret);
1129
1130 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1131 auto const& newState = newLoanProperties.loanState;
1132
1133 // =========== VALIDATE PAYMENT PARTS ===========
1134
1135 // Since there is loan management fee, the fee is charged against
1136 // overpayment interest portion first, so interest paid remains 4.5
1137 BEAST_EXPECTS(
1138 (actualPaymentParts.interestPaid == Number{45, -1}),
1139 " interestPaid mismatch: expected 4.5, got " +
1140 to_string(actualPaymentParts.interestPaid));
1141
1142 // With overpayment interest portion, value change should equal the
1143 // interest decrease plus overpayment interest portion
1144 BEAST_EXPECTS(
1145 (actualPaymentParts.valueChange ==
1146 Number{-164737, -5} + actualPaymentParts.interestPaid),
1147 " valueChange mismatch: expected " +
1148 to_string(Number{-164737, -5} + actualPaymentParts.interestPaid) + ", got " +
1149 to_string(actualPaymentParts.valueChange));
1150
1151 // While there is no overpayment fee, fee paid should equal the
1152 // management fee charged against the overpayment interest portion
1153 BEAST_EXPECTS(
1154 (actualPaymentParts.feePaid == Number{55, -1}),
1155 " feePaid mismatch: expected 5.5, got " + to_string(actualPaymentParts.feePaid));
1156
1157 BEAST_EXPECTS(
1158 actualPaymentParts.principalPaid == 40,
1159 " principalPaid mismatch: expected 40, got `" +
1160 to_string(actualPaymentParts.principalPaid));
1161
1162 // =========== VALIDATE STATE CHANGES ===========
1163
1164 BEAST_EXPECTS(
1165 actualPaymentParts.principalPaid ==
1166 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1167 " principalPaid mismatch: expected " +
1168 to_string(
1169 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1170 ", got " + to_string(actualPaymentParts.principalPaid));
1171
1172 // Note that the management fee value change is not captured, as this
1173 // value is not needed to correctly update the Vault state.
1174 BEAST_EXPECTS(
1175 (newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
1176 Number{-18304, -5}),
1177 " management fee change mismatch: expected " + to_string(Number{-18304, -5}) +
1178 ", got " +
1179 to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
1180
1181 BEAST_EXPECTS(
1182 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1183 newState.interestDue - loanProperties.loanState.interestDue,
1184 " valueChange mismatch: expected " +
1185 to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
1186 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
1187 }
1188
1189public:
1190 void
1210};
1211
1212BEAST_DEFINE_TESTSUITE(LendingHelpers, app, xrpl);
1213
1214} // namespace test
1215} // namespace xrpl
A testsuite class.
Definition suite.h:51
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:150
Number is a floating point type that can represent a wide range of values.
Definition Number.h:207
void run() override
Runs the suite.
Immutable cryptographic account descriptor.
Definition Account.h:19
A transaction testing environment.
Definition Env.h:122
Converts to IOU Issue or STAmount.
ExtendedPaymentComponents computeOverpaymentComponents(Asset const &asset, int32_t const loanScale, Number const &overpayment, TenthBips32 const overpaymentInterestRate, TenthBips32 const overpaymentFeeRate, TenthBips16 const managementFeeRate)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:602
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:437
LoanProperties computeLoanProperties(Asset const &asset, Number const &principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
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)