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