xrpld
Loading...
Searching...
No Matches
LendingHelpers_test.cpp
1#include <xrpl/beast/unit_test/suite.h>
2// DO NOT REMOVE
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/amount.h>
6
7#include <xrpl/basics/Number.h>
8#include <xrpl/basics/chrono.h>
9#include <xrpl/ledger/helpers/LendingHelpers.h>
10#include <xrpl/protocol/Feature.h>
11#include <xrpl/protocol/LedgerFormats.h>
12#include <xrpl/protocol/Protocol.h>
13#include <xrpl/protocol/SField.h>
14#include <xrpl/protocol/STAmount.h>
15#include <xrpl/protocol/STLedgerEntry.h>
16#include <xrpl/protocol/TER.h>
17#include <xrpl/protocol/Units.h>
18
19#include <cstdint>
20#include <memory>
21#include <optional>
22#include <string>
23#include <utility>
24#include <vector>
25
26namespace xrpl::test {
27
29{
30 void
32 {
33 using namespace jtx;
34 using namespace xrpl::detail;
35 Env const env{*this};
36 auto const& rules = env.current()->rules();
37 struct TestCase
38 {
39 std::string name;
40 Number periodicRate;
41 std::uint32_t paymentsRemaining;
42 Number expectedPaymentFactor;
43 };
44
45 auto const testCases = std::vector<TestCase>{
46 {
47 .name = "Zero periodic rate",
48 .periodicRate = Number{0},
49 .paymentsRemaining = 4,
50 .expectedPaymentFactor = Number{25, -2},
51 }, // 1/4 = 0.25
52 {
53 .name = "One payment remaining",
54 .periodicRate = Number{5, -2},
55 .paymentsRemaining = 1,
56 .expectedPaymentFactor = Number{105, -2},
57 }, // 0.05/1 = 1.05
58 {
59 .name = "Multiple payments remaining",
60 .periodicRate = Number{5, -2},
61 .paymentsRemaining = 3,
62 .expectedPaymentFactor = Number{3672085646312450436, -19},
63 }, // from calc
64 {
65 .name = "Zero payments remaining",
66 .periodicRate = Number{5, -2},
67 .paymentsRemaining = 0,
68 .expectedPaymentFactor = Number{0},
69 } // edge case
70 };
71
72 for (auto const& tc : testCases)
73 {
74 testcase("computePaymentFactor: " + tc.name);
75
76 auto const computedPaymentFactor =
77 computePaymentFactor(rules, tc.periodicRate, tc.paymentsRemaining);
78 BEAST_EXPECTS(
79 computedPaymentFactor == tc.expectedPaymentFactor,
80 "Payment factor mismatch: expected " + to_string(tc.expectedPaymentFactor) +
81 ", got " + to_string(computedPaymentFactor));
82 }
83 }
84
85 void
87 {
88 using namespace jtx;
89 using namespace xrpl::detail;
90 Env const env{*this};
91 auto const& rules = env.current()->rules();
92
93 struct TestCase
94 {
95 std::string name;
96 Number principalOutstanding;
97 Number periodicRate;
98 std::uint32_t paymentsRemaining;
99 Number expectedPeriodicPayment;
100 };
101
102 auto const testCases = std::vector<TestCase>{
103 {
104 .name = "Zero principal outstanding",
105 .principalOutstanding = Number{0},
106 .periodicRate = Number{5, -2},
107 .paymentsRemaining = 5,
108 .expectedPeriodicPayment = Number{0},
109 },
110 {
111 .name = "Zero payments remaining",
112 .principalOutstanding = Number{1'000},
113 .periodicRate = Number{5, -2},
114 .paymentsRemaining = 0,
115 .expectedPeriodicPayment = Number{0},
116 },
117 {
118 .name = "Zero periodic rate",
119 .principalOutstanding = Number{1'000},
120 .periodicRate = Number{0},
121 .paymentsRemaining = 4,
122 .expectedPeriodicPayment = Number{250},
123 },
124 {
125 .name = "Standard case",
126 .principalOutstanding = Number{1'000},
127 .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
128 .paymentsRemaining = 3,
129 .expectedPeriodicPayment = Number{389569066396123265, -15}, // from calc
130 },
131 };
132
133 for (auto const& tc : testCases)
134 {
135 testcase("loanPeriodicPayment: " + tc.name);
136
137 auto const computedPeriodicPayment = loanPeriodicPayment(
138 rules, tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
139 BEAST_EXPECTS(
140 computedPeriodicPayment == tc.expectedPeriodicPayment,
141 "Periodic payment mismatch: expected " + to_string(tc.expectedPeriodicPayment) +
142 ", got " + to_string(computedPeriodicPayment));
143 }
144 }
145
146 void
148 {
149 using namespace jtx;
150 using namespace xrpl::detail;
151 Env const env{*this};
152 auto const& rules = env.current()->rules();
153
154 struct TestCase
155 {
156 std::string name;
157 Number periodicPayment;
158 Number periodicRate;
159 std::uint32_t paymentsRemaining;
160 Number expectedPrincipalOutstanding;
161 };
162
163 auto const testCases = std::vector<TestCase>{
164 {
165 .name = "Zero periodic payment",
166 .periodicPayment = Number{0},
167 .periodicRate = Number{5, -2},
168 .paymentsRemaining = 5,
169 .expectedPrincipalOutstanding = Number{0},
170 },
171 {
172 .name = "Zero payments remaining",
173 .periodicPayment = Number{1'000},
174 .periodicRate = Number{5, -2},
175 .paymentsRemaining = 0,
176 .expectedPrincipalOutstanding = Number{0},
177 },
178 {
179 .name = "Zero periodic rate",
180 .periodicPayment = Number{250},
181 .periodicRate = Number{0},
182 .paymentsRemaining = 4,
183 .expectedPrincipalOutstanding = Number{1'000},
184 },
185 {
186 .name = "Standard case",
187 .periodicPayment = Number{389569066396123265, -15}, // from calc
188 .periodicRate = loanPeriodicRate(TenthBips32(100'000), 30 * 24 * 60 * 60),
189 .paymentsRemaining = 3,
190 .expectedPrincipalOutstanding = Number{1'000},
191 },
192 };
193
194 for (auto const& tc : testCases)
195 {
196 testcase("loanPrincipalFromPeriodicPayment: " + tc.name);
197
198 auto const computedPrincipalOutstanding = loanPrincipalFromPeriodicPayment(
199 rules, tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
200 BEAST_EXPECTS(
201 computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
202 "Principal outstanding mismatch: expected " +
203 to_string(tc.expectedPrincipalOutstanding) + ", got " +
204 to_string(computedPrincipalOutstanding));
205 }
206 }
207
208 void
210 {
211 using namespace jtx;
212 using namespace xrpl::detail;
213
214 // Edge cases.
215 {
216 testcase("computePowerMinusOne: zero rate returns zero");
217 BEAST_EXPECT(computePowerMinusOne(0, 5) == 0);
218 }
219 {
220 testcase("computePowerMinusOne: zero paymentsRemaining returns zero");
221 Number const fivePercent{5, -2};
222 BEAST_EXPECT(computePowerMinusOne(fivePercent, 0) == 0);
223 }
224 // (1.05)^3 - 1 = 0.157625, computed independently by hand.
225 {
226 testcase("computePowerMinusOne: standard case (1.05)^3 - 1 = 0.157625");
227 Number const r{5, -2};
228 Number const expected{157625, -6};
229 BEAST_EXPECT(computePowerMinusOne(r, 3) == expected);
230 }
231 // (1+1)^1 - 1 = 1.
232 {
233 testcase("computePowerMinusOne: r=1, n=1");
234 BEAST_EXPECT(computePowerMinusOne(1, 1) == 1);
235 }
236
237 // Property check at near-zero rate (the bug regime): for n=2 the
238 // mathematical identity is `(1+r)^2 - 1 = 2r + r^2`. We compute
239 // `2r + r^2` by direct multiplication in Number arithmetic — a
240 // path that doesn't share any code with the binomial loop — and
241 // assert the two paths agree.
242 {
243 testcase("computePowerMinusOne: near-zero rate matches independent 2r + r^2");
244 // r = 1 TenthBips32 over 600s payment interval, computed
245 // independently below using xrpl::detail::loanPeriodicRate.
246 Number const r = loanPeriodicRate(TenthBips32{1}, 600);
247 Number const independentExpected = 2 * r + r * r; // (1+r)^2 - 1
248 BEAST_EXPECT(computePowerMinusOne(r, 2) == independentExpected);
249 }
250 // Same property at n=3: (1+r)^3 - 1 = 3r + 3r^2 + r^3.
251 {
252 testcase("computePowerMinusOne: near-zero rate matches independent 3r + 3r^2 + r^3");
253 Number const r = loanPeriodicRate(TenthBips32{1}, 600);
254 Number const independentExpected = 3 * r + 3 * r * r + r * r * r;
255 BEAST_EXPECT(computePowerMinusOne(r, 3) == independentExpected);
256 }
257
258 // Larger-n stress test for the loop's early-termination logic.
259 // At very small r the binomial terms decrease by a factor of
260 // ~r*(n-k)/(k+1) per step, so even at n=1000 the loop should
261 // terminate in a small handful of iterations. Cross-check the
262 // result against the hybrid (which dispatches to this same
263 // binomial path when r*n < 1e-9).
264 {
265 testcase("computePowerMinusOne: large n, early termination matches hybrid output");
266 // r*n = 1e-10 and 1e-12 — both clearly below the 1e-9 threshold.
267 Number const r1{1, -13};
268 std::uint32_t const n1 = 1'000;
269 Number const r2{1, -15};
270 std::uint32_t const n2 = 1'000;
271 BEAST_EXPECT(computePowerMinusOne(r1, n1) == computePowerMinusOneHybrid(r1, n1));
272 BEAST_EXPECT(computePowerMinusOne(r2, n2) == computePowerMinusOneHybrid(r2, n2));
273 BEAST_EXPECT(computePowerMinusOne(r1, n1) > 0);
274 BEAST_EXPECT(computePowerMinusOne(r2, n2) > 0);
275 }
276 }
277
278 // Direct tests of `computePowerMinusOneHybrid`. Verifies the dispatcher
279 // picks the right branch and produces the right result on each side
280 // of the threshold.
281 void
283 {
284 using namespace jtx;
285 using namespace xrpl::detail;
286
287 // Above threshold (r * n >= 1e-9): hybrid must agree with the closed
288 // form `power(1+r, n) - 1` exactly (it is the closed form).
289 {
290 testcase("computePowerMinusOneHybrid: r*n >= 1e-9 uses closed form (bit-exact match)");
291
292 struct AboveThreshold
293 {
294 std::string name;
295 Number r;
297 };
298 auto const cases = std::vector<AboveThreshold>{
299 {.name = "r=5%, n=3", .r = Number{5, -2}, .n = 3},
300 {.name = "r=0.1%, n=1000", .r = Number{1, -3}, .n = 1'000},
301 {.name = "r=1e-7, n=100 (above threshold by 10x)", .r = Number{1, -7}, .n = 100},
302 };
303 for (auto const& tc : cases)
304 {
305 Number const closed = power(1 + tc.r, tc.n) - 1;
306 Number const hybrid = computePowerMinusOneHybrid(tc.r, tc.n);
307 BEAST_EXPECTS(
308 hybrid == closed,
309 tc.name + ": closed=" + to_string(closed) + ", hybrid=" + to_string(hybrid));
310 }
311 }
312
313 // Below threshold (r * n < 1e-9): hybrid must agree with
314 // `computePowerMinusOne` (the binomial expansion). At this regime
315 // the closed form is provably wrong (cancellation); we verify the
316 // dispatcher routes to the binomial path.
317 {
318 testcase(
319 "computePowerMinusOneHybrid: r*n < 1e-9 uses binomial expansion (bit-exact match)");
320
321 struct BelowThreshold
322 {
323 std::string name;
324 Number r;
326 };
327 auto const cases = std::vector<BelowThreshold>{
328 // bug regime: r = 1 TenthBips32 over 600s payment interval
329 // → r ≈ 1.9e-10, r*n ≈ 3.8e-10 < 1e-9.
330 {.name = "bug regime: r~1.9e-10, n=2",
331 .r = loanPeriodicRate(TenthBips32{1}, 600),
332 .n = 2},
333 {.name = "r=1e-12, n=100", .r = Number{1, -12}, .n = 100},
334 };
335 for (auto const& tc : cases)
336 {
337 Number const binom = computePowerMinusOne(tc.r, tc.n);
338 Number const hybrid = computePowerMinusOneHybrid(tc.r, tc.n);
339 BEAST_EXPECTS(
340 hybrid == binom,
341 tc.name + ": binom=" + to_string(binom) + ", hybrid=" + to_string(hybrid));
342 }
343 }
344
345 // Edge cases.
346 {
347 testcase("computePowerMinusOneHybrid: edge cases");
348 Number const fivePercent{5, -2};
349 BEAST_EXPECT(computePowerMinusOneHybrid(0, 100) == 0);
350 BEAST_EXPECT(computePowerMinusOneHybrid(fivePercent, 0) == 0);
351 BEAST_EXPECT(computePowerMinusOneHybrid(0, 0) == 0);
352 }
353
354 // Threshold boundary: r*n = 1e-9 exactly. Hybrid uses `>=` against
355 // the threshold, so this case must take the closed-form branch.
356 // We also verify that the binomial path agrees with the closed
357 // form to high precision at this crossover — confirming the
358 // threshold is placed where both paths give "adequate" answers.
359 {
360 testcase("computePowerMinusOneHybrid: threshold boundary r*n = 1e-9");
361
362 // Construct exactly r*n = 1e-9 with two distinct (r, n) pairs.
363 struct Boundary
364 {
365 std::string name;
366 Number r;
368 };
369 auto const cases = std::vector<Boundary>{
370 {.name = "r=1e-9, n=1", .r = Number{1, -9}, .n = 1},
371 {.name = "r=1e-12, n=1000", .r = Number{1, -12}, .n = 1'000},
372 };
373
374 for (auto const& tc : cases)
375 {
376 Number const closed = power(1 + tc.r, tc.n) - 1;
377 Number const hybrid = computePowerMinusOneHybrid(tc.r, tc.n);
378 Number const binom = computePowerMinusOne(tc.r, tc.n);
379
380 // At exact threshold, hybrid must take closed-form path:
381 // bit-exact match with closed.
382 BEAST_EXPECTS(
383 hybrid == closed,
384 tc.name + ": hybrid should equal closed at threshold; got hybrid=" +
385 to_string(hybrid) + ", closed=" + to_string(closed));
386
387 // Closed-form and binomial must agree at the threshold to
388 // within Number's post-subtraction precision (~10 sig
389 // digits of `r*n = 1e-9`, i.e. ~1e-19 absolute error).
390 Number const tolerance{1, -18};
391 Number const diff = abs(closed - binom);
392 BEAST_EXPECTS(
393 diff < tolerance,
394 tc.name + ": closed and binomial diverge at threshold by " + to_string(diff));
395 }
396 }
397 }
398
399 // Regression: at near-zero rate, `loanPrincipalFromPeriodicPayment`
400 // must satisfy `principal <= periodicPayment * paymentsRemaining` for
401 // any non-negative rate. The naive closed-form path violated this
402 // bound due to catastrophic cancellation in `(1+r)^n - 1`.
403 void
405 {
406 testcase("loanPrincipalFromPeriodicPayment: principal <= payment*n at near-zero rate");
407 using namespace jtx;
408 using namespace xrpl::detail;
409 Env const env{*this};
410 auto const& rules = env.current()->rules();
411
412 // Inputs from the bug reproduction in Loan_test.cpp:
413 // InterestRate = 1 TenthBips32 (0.001 % per year),
414 // PaymentInterval = 600 s, principal = 100, 3 payments.
415 // periodicRate is ~1.9e-10.
416 auto const periodicRate = loanPeriodicRate(TenthBips32{1}, 600);
417 auto const periodicPayment = loanPeriodicPayment(rules, 100, periodicRate, 3);
418
419 for (auto const n : {3u, 2u, 1u})
420 {
421 auto const computed =
422 loanPrincipalFromPeriodicPayment(rules, periodicPayment, periodicRate, n);
423 auto const upperBound = periodicPayment * Number{n};
424 BEAST_EXPECTS(
425 computed <= upperBound,
426 "n=" + std::to_string(n) + ": payment*n=" + to_string(upperBound) +
427 ", principal=" + to_string(computed));
428 }
429 }
430
431 // Regression: `computeTheoreticalLoanState` must produce a non-negative
432 // `interestDue` for any non-negative rate. Pre-fix, near-zero rates
433 // produced a negative `interestDue` because `(1+r)^n - 1` lost most of
434 // its precision to cancellation.
435 void
437 {
438 testcase("computeTheoreticalLoanState: non-negative interestDue at near-zero rate");
439 using namespace jtx;
440 using namespace xrpl::detail;
441 Env const env{*this};
442 auto const& rules = env.current()->rules();
443
444 auto const periodicRate = loanPeriodicRate(TenthBips32{1}, 600);
445 auto const periodicPayment = loanPeriodicPayment(rules, 100, periodicRate, 3);
446
447 auto const state =
448 computeTheoreticalLoanState(rules, periodicPayment, periodicRate, 2, TenthBips32{0});
449
450 BEAST_EXPECT(state.principalOutstanding <= state.valueOutstanding);
451 BEAST_EXPECT(state.interestDue >= 0);
452 BEAST_EXPECT(state.managementFeeDue == 0);
453 }
454
455 // Direct gating proof: at near-zero rate, `computePaymentFactor` must
456 // return different values with `fixCleanup3_2_0` disabled vs enabled.
457 // The enabled path agrees with an independent polynomial reference;
458 // the disabled path diverges by a measurable amount due to the
459 // catastrophic cancellation in `(1+r)^n - 1`.
460 void
462 {
463 testcase("computePaymentFactor: near-zero rate, amendment disabled vs enabled");
464 using namespace jtx;
465 using namespace xrpl::detail;
466
467 Number const r = loanPeriodicRate(TenthBips32{1}, 600);
468 std::uint32_t const n = 3;
469
470 // Independent reference: expand F(r,3) = r*(1+r)^3/((1+r)^3-1)
471 // algebraically for n=3, dividing numerator and denominator by r:
472 // F(r,3) = (1 + 3r + 3r^2 + r^3) / (3 + 3r + r^2)
473 // No power(), no binomial series — pure polynomial arithmetic in
474 // Number.
475 Number const reference = (1 + 3 * r + 3 * r * r + r * r * r) / (3 + 3 * r + r * r);
476
477 // Pre-fix: closed form power(1+r, n) - 1 suffers catastrophic
478 // cancellation when r*n ~ 5.7e-10.
479 Env const envBug{*this, testableAmendments() - fixCleanup3_2_0};
480 Number const buggyFactor = computePaymentFactor(envBug.current()->rules(), r, n);
481
482 // Post-fix: hybrid binomial path avoids cancellation.
483 Env const envFix{*this};
484 Number const correctFactor = computePaymentFactor(envFix.current()->rules(), r, n);
485
486 // The amendment must change the computed factor in this regime.
487 BEAST_EXPECT(buggyFactor != correctFactor);
488
489 // The fixed factor must agree with the polynomial reference to
490 // within a few ULPs of Number's 19-digit precision.
491 BEAST_EXPECT(abs(correctFactor - reference) < Number(1, -15));
492
493 // The buggy factor must diverge from the reference by a measurable
494 // amount — empirically ~1e-10 in this regime.
495 BEAST_EXPECT(abs(buggyFactor - reference) > Number(1, -12));
496 }
497
498 void
500 {
501 testcase("computeOverpaymentComponents");
502 using namespace jtx;
503 using namespace xrpl::detail;
504
505 Account const issuer{"issuer"};
506 PrettyAsset const iou = issuer["IOU"];
507 int32_t const loanScale = 1;
508 auto const overpayment = Number{1'000};
509 auto const overpaymentInterestRate = TenthBips32{10'000}; // 10%
510 auto const overpaymentFeeRate = TenthBips32{50'000}; // 50%
511 auto const managementFeeRate = TenthBips16{10'000}; // 10%
512
513 auto const expectedOverpaymentFee = Number{500}; // 50% of 1,000
514 auto const expectedOverpaymentInterestGross = Number{100}; // 10% of 1,000
515 auto const expectedOverpaymentInterestNet = Number{90}; // 100 - 10% of 100
516 auto const expectedOverpaymentManagementFee = Number{10}; // 10% of 100
517 auto const expectedPrincipalPortion = Number{400}; // 1,000 - 100 - 500
518
519 Env const env{*this};
520 auto const components = xrpl::detail::computeOverpaymentComponents(
521 env.current()->rules(),
522 iou,
523 loanScale,
524 overpayment,
525 overpaymentInterestRate,
526 overpaymentFeeRate,
527 managementFeeRate);
528
529 BEAST_EXPECT(components.untrackedManagementFee == expectedOverpaymentFee);
530
531 BEAST_EXPECT(components.untrackedInterest == expectedOverpaymentInterestNet);
532
533 BEAST_EXPECT(components.trackedInterestPart() == expectedOverpaymentInterestNet);
534
535 BEAST_EXPECT(components.trackedManagementFeeDelta == expectedOverpaymentManagementFee);
536 BEAST_EXPECT(components.trackedPrincipalDelta == expectedPrincipalPortion);
537 BEAST_EXPECT(
538 components.trackedManagementFeeDelta + components.untrackedInterest ==
539 expectedOverpaymentInterestGross);
540
541 BEAST_EXPECT(
542 components.trackedManagementFeeDelta + components.untrackedInterest +
543 components.trackedPrincipalDelta + components.untrackedManagementFee ==
544 overpayment);
545 }
546
547 void
549 {
550 using namespace jtx;
551 using namespace xrpl::detail;
552
553 struct TestCase
554 {
555 std::string name;
556 Number interest;
557 TenthBips16 managementFeeRate;
558 Number expectedInterestPart;
559 Number expectedFeePart;
560 };
561
562 Account const issuer{"issuer"};
563 PrettyAsset const iou = issuer["IOU"];
564 std::int32_t const loanScale = 1;
565
566 auto const testCases = std::vector<TestCase>{
567 {.name = "Zero interest",
568 .interest = Number{0},
569 .managementFeeRate = TenthBips16{10'000},
570 .expectedInterestPart = Number{0},
571 .expectedFeePart = Number{0}},
572 {.name = "Zero fee rate",
573 .interest = Number{1'000},
574 .managementFeeRate = TenthBips16{0},
575 .expectedInterestPart = Number{1'000},
576 .expectedFeePart = Number{0}},
577 {.name = "10% fee rate",
578 .interest = Number{1'000},
579 .managementFeeRate = TenthBips16{10'000},
580 .expectedInterestPart = Number{900},
581 .expectedFeePart = Number{100}},
582 };
583
584 for (auto const& tc : testCases)
585 {
586 testcase("computeInterestAndFeeParts: " + tc.name);
587
588 auto const [computedInterestPart, computedFeePart] =
589 computeInterestAndFeeParts(iou, tc.interest, tc.managementFeeRate, loanScale);
590 BEAST_EXPECTS(
591 computedInterestPart == tc.expectedInterestPart,
592 "Interest part mismatch: expected " + to_string(tc.expectedInterestPart) +
593 ", got " + to_string(computedInterestPart));
594 BEAST_EXPECTS(
595 computedFeePart == tc.expectedFeePart,
596 "Fee part mismatch: expected " + to_string(tc.expectedFeePart) + ", got " +
597 to_string(computedFeePart));
598 }
599 }
600
601 void
603 {
604 using namespace jtx;
605 using namespace xrpl::detail;
606 struct TestCase
607 {
608 std::string name;
609 Number principalOutstanding;
610 TenthBips32 lateInterestRate;
611 NetClock::time_point parentCloseTime;
612 std::uint32_t nextPaymentDueDate;
613 Number expectedLateInterest;
614 };
615
616 auto const testCases = std::vector<TestCase>{
617 {
618 .name = "On-time payment",
619 .principalOutstanding = Number{1'000},
620 .lateInterestRate = TenthBips32{10'000}, // 10%
621 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
622 .nextPaymentDueDate = 3'000,
623 .expectedLateInterest = Number{0},
624 },
625 {
626 .name = "Early payment",
627 .principalOutstanding = Number{1'000},
628 .lateInterestRate = TenthBips32{10'000}, // 10%
629 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
630 .nextPaymentDueDate = 4'000,
631 .expectedLateInterest = Number{0},
632 },
633 {
634 .name = "No principal outstanding",
635 .principalOutstanding = Number{0},
636 .lateInterestRate = TenthBips32{10'000}, // 10%
637 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
638 .nextPaymentDueDate = 2'000,
639 .expectedLateInterest = Number{0},
640 },
641 {
642 .name = "No late interest rate",
643 .principalOutstanding = Number{1'000},
644 .lateInterestRate = TenthBips32{0}, // 0%
645 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
646 .nextPaymentDueDate = 2'000,
647 .expectedLateInterest = Number{0},
648 },
649 {
650 .name = "Late payment",
651 .principalOutstanding = Number{1'000},
652 .lateInterestRate = TenthBips32{100'000}, // 100%
653 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
654 .nextPaymentDueDate = 2'000,
655 .expectedLateInterest = Number{317097919837645865, -19}, // from calc
656 },
657 };
658
659 for (auto const& tc : testCases)
660 {
661 testcase("loanLatePaymentInterest: " + tc.name);
662
663 auto const computedLateInterest = loanLatePaymentInterest(
664 tc.principalOutstanding,
665 tc.lateInterestRate,
666 tc.parentCloseTime,
667 tc.nextPaymentDueDate);
668 BEAST_EXPECTS(
669 computedLateInterest == tc.expectedLateInterest,
670 "Late interest mismatch: expected " + to_string(tc.expectedLateInterest) +
671 ", got " + to_string(computedLateInterest));
672 }
673 }
674
675 void
677 {
678 using namespace jtx;
679 using namespace xrpl::detail;
680 struct TestCase
681 {
682 std::string name;
683 Number principalOutstanding;
684 Number periodicRate;
685 NetClock::time_point parentCloseTime;
686 std::uint32_t startDate;
687 std::uint32_t prevPaymentDate;
688 std::uint32_t paymentInterval;
689 Number expectedAccruedInterest;
690 };
691
692 auto const testCases = std::vector<TestCase>{
693 {
694 .name = "Zero principal outstanding",
695 .principalOutstanding = Number{0},
696 .periodicRate = Number{5, -2},
697 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
698 .startDate = 2'000,
699 .prevPaymentDate = 2'500,
700 .paymentInterval = 30 * 24 * 60 * 60,
701 .expectedAccruedInterest = Number{0},
702 },
703 {
704 .name = "Before start date",
705 .principalOutstanding = Number{1'000},
706 .periodicRate = Number{5, -2},
707 .parentCloseTime = NetClock::time_point{NetClock::duration{1'000}},
708 .startDate = 2'000,
709 .prevPaymentDate = 1'500,
710 .paymentInterval = 30 * 24 * 60 * 60,
711 .expectedAccruedInterest = Number{0},
712 },
713 {
714 .name = "Zero periodic rate",
715 .principalOutstanding = Number{1'000},
716 .periodicRate = Number{0},
717 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
718 .startDate = 2'000,
719 .prevPaymentDate = 2'500,
720 .paymentInterval = 30 * 24 * 60 * 60,
721 .expectedAccruedInterest = Number{0},
722 },
723 {
724 .name = "Zero payment interval",
725 .principalOutstanding = Number{1'000},
726 .periodicRate = Number{5, -2},
727 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
728 .startDate = 2'000,
729 .prevPaymentDate = 2'500,
730 .paymentInterval = 0,
731 .expectedAccruedInterest = Number{0},
732 },
733 {
734 .name = "Standard case",
735 .principalOutstanding = Number{1'000},
736 .periodicRate = Number{5, -2},
737 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
738 .startDate = 1'000,
739 .prevPaymentDate = 2'000,
740 .paymentInterval = 30 * 24 * 60 * 60,
741 .expectedAccruedInterest = Number{1929012345679012346, -20}, // from calc
742 },
743 };
744
745 for (auto const& tc : testCases)
746 {
747 testcase("loanAccruedInterest: " + tc.name);
748
749 auto const computedAccruedInterest = loanAccruedInterest(
750 tc.principalOutstanding,
751 tc.periodicRate,
752 tc.parentCloseTime,
753 tc.startDate,
754 tc.prevPaymentDate,
755 tc.paymentInterval);
756 BEAST_EXPECTS(
757 computedAccruedInterest == tc.expectedAccruedInterest,
758 "Accrued interest mismatch: expected " + to_string(tc.expectedAccruedInterest) +
759 ", got " + to_string(computedAccruedInterest));
760 }
761 }
762
763 // This test overlaps with testLoanAccruedInterest, the test cases only
764 // exercise the computeFullPaymentInterest parts unique to it.
765 void
767 {
768 using namespace jtx;
769 using namespace xrpl::detail;
770
771 struct TestCase
772 {
773 std::string name;
774 Number rawPrincipalOutstanding;
775 Number periodicRate;
776 NetClock::time_point parentCloseTime;
777 std::uint32_t paymentInterval;
778 std::uint32_t prevPaymentDate;
779 std::uint32_t startDate;
780 TenthBips32 closeInterestRate;
781 Number expectedFullPaymentInterest;
782 };
783
784 auto const testCases = std::vector<TestCase>{
785 {
786 .name = "Zero principal outstanding",
787 .rawPrincipalOutstanding = Number{0},
788 .periodicRate = Number{5, -2},
789 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
790 .paymentInterval = 30 * 24 * 60 * 60,
791 .prevPaymentDate = 2'000,
792 .startDate = 1'000,
793 .closeInterestRate = TenthBips32{10'000},
794 .expectedFullPaymentInterest = Number{0},
795 },
796 {
797 .name = "Zero close interest rate",
798 .rawPrincipalOutstanding = Number{1'000},
799 .periodicRate = Number{5, -2},
800 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
801 .paymentInterval = 30 * 24 * 60 * 60,
802 .prevPaymentDate = 2'000,
803 .startDate = 1'000,
804 .closeInterestRate = TenthBips32{0},
805 .expectedFullPaymentInterest = Number{1929012345679012346, -20}, // from calc
806 },
807 {
808 .name = "Standard case",
809 .rawPrincipalOutstanding = Number{1'000},
810 .periodicRate = Number{5, -2},
811 .parentCloseTime = NetClock::time_point{NetClock::duration{3'000}},
812 .paymentInterval = 30 * 24 * 60 * 60,
813 .prevPaymentDate = 2'000,
814 .startDate = 1'000,
815 .closeInterestRate = TenthBips32{10'000},
816 .expectedFullPaymentInterest = Number{1000192901234567901, -16}, // from calc
817 },
818 };
819
820 for (auto const& tc : testCases)
821 {
822 testcase("computeFullPaymentInterest: " + tc.name);
823
824 auto const computedFullPaymentInterest = computeFullPaymentInterest(
825 tc.rawPrincipalOutstanding,
826 tc.periodicRate,
827 tc.parentCloseTime,
828 tc.paymentInterval,
829 tc.prevPaymentDate,
830 tc.startDate,
831 tc.closeInterestRate);
832 BEAST_EXPECTS(
833 computedFullPaymentInterest == tc.expectedFullPaymentInterest,
834 "Full payment interest mismatch: expected " +
835 to_string(tc.expectedFullPaymentInterest) + ", got " +
836 to_string(computedFullPaymentInterest));
837 }
838 }
839
840 void
842 {
843 // This test ensures that overpayment with no interest works correctly.
844 testcase("tryOverpayment - No Interest No Fee");
845
846 using namespace jtx;
847 using namespace xrpl::detail;
848
849 Env const env{*this};
850 Account const issuer{"issuer"};
851 PrettyAsset const asset = issuer["USD"];
852 std::int32_t const loanScale = -5;
853 TenthBips16 const managementFeeRate{0}; // 0%
854 TenthBips32 const loanInterestRate{0}; // 0%
855 Number const loanPrincipal{1'000};
856 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
857 std::uint32_t const paymentsRemaining = 10;
858 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
859 Number const overpaymentAmount{50};
860
861 auto const overpaymentComponents = computeOverpaymentComponents(
862 env.current()->rules(),
863 asset,
864 loanScale,
865 overpaymentAmount,
866 TenthBips32(0),
867 TenthBips32(0),
868 managementFeeRate);
869
870 auto const loanProperties = computeLoanProperties(
871 env.current()->rules(),
872 asset,
873 loanPrincipal,
874 loanInterestRate,
875 paymentInterval,
876 paymentsRemaining,
877 managementFeeRate,
878 loanScale);
879
880 auto const ret = tryOverpayment(
881 env.current()->rules(),
882 asset,
883 loanScale,
884 overpaymentComponents,
885 loanProperties.loanState,
886 loanProperties.periodicPayment,
887 periodicRate,
888 paymentsRemaining,
889 managementFeeRate,
890 env.journal);
891
892 BEAST_EXPECT(ret);
893
894 auto const& [actualPaymentParts, newLoanProperties] = *ret;
895 auto const& newState = newLoanProperties.loanState;
896
897 // =========== VALIDATE PAYMENT PARTS ===========
898 BEAST_EXPECTS(
899 actualPaymentParts.valueChange == 0,
900 " valueChange mismatch: expected 0, got " + to_string(actualPaymentParts.valueChange));
901
902 BEAST_EXPECTS(
903 actualPaymentParts.feePaid == 0,
904 " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid));
905
906 BEAST_EXPECTS(
907 actualPaymentParts.interestPaid == 0,
908 " interestPaid mismatch: expected 0, got " +
909 to_string(actualPaymentParts.interestPaid));
910
911 BEAST_EXPECTS(
912 actualPaymentParts.principalPaid == overpaymentAmount,
913 " principalPaid mismatch: expected " + to_string(overpaymentAmount) + ", got " +
914 to_string(actualPaymentParts.principalPaid));
915
916 // =========== VALIDATE STATE CHANGES ===========
917 BEAST_EXPECTS(
918 loanProperties.loanState.interestDue - newState.interestDue == 0,
919 " interest change mismatch: expected 0, got " +
920 to_string(loanProperties.loanState.interestDue - newState.interestDue));
921
922 BEAST_EXPECTS(
923 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
924 " management fee change mismatch: expected 0, got " +
925 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
926
927 BEAST_EXPECTS(
928 actualPaymentParts.principalPaid ==
929 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
930 " principalPaid mismatch: expected " +
931 to_string(
932 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
933 ", got " + to_string(actualPaymentParts.principalPaid));
934 }
935
936 void
938 {
939 testcase("tryOverpayment - No Interest With Overpayment Fee");
940
941 using namespace jtx;
942 using namespace xrpl::detail;
943
944 Env const env{*this};
945 Account const issuer{"issuer"};
946 PrettyAsset const asset = issuer["USD"];
947 std::int32_t const loanScale = -5;
948 TenthBips16 const managementFeeRate{0}; // 0%
949 TenthBips32 const loanInterestRate{0}; // 0%
950 Number const loanPrincipal{1'000};
951 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
952 std::uint32_t const paymentsRemaining = 10;
953 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
954
955 auto const overpaymentComponents = computeOverpaymentComponents(
956 env.current()->rules(),
957 asset,
958 loanScale,
959 Number{50, 0},
960 TenthBips32(0),
961 TenthBips32(10'000), // 10% overpayment fee
962 managementFeeRate);
963
964 auto const loanProperties = computeLoanProperties(
965 env.current()->rules(),
966 asset,
967 loanPrincipal,
968 loanInterestRate,
969 paymentInterval,
970 paymentsRemaining,
971 managementFeeRate,
972 loanScale);
973
974 auto const ret = tryOverpayment(
975 env.current()->rules(),
976 asset,
977 loanScale,
978 overpaymentComponents,
979 loanProperties.loanState,
980 loanProperties.periodicPayment,
981 periodicRate,
982 paymentsRemaining,
983 managementFeeRate,
984 env.journal);
985
986 BEAST_EXPECT(ret);
987
988 auto const& [actualPaymentParts, newLoanProperties] = *ret;
989 auto const& newState = newLoanProperties.loanState;
990
991 // =========== VALIDATE PAYMENT PARTS ===========
992 BEAST_EXPECTS(
993 actualPaymentParts.valueChange == 0,
994 " valueChange mismatch: expected 0, got " + to_string(actualPaymentParts.valueChange));
995
996 BEAST_EXPECTS(
997 actualPaymentParts.feePaid == 5,
998 " feePaid mismatch: expected 5, got " + to_string(actualPaymentParts.feePaid));
999
1000 BEAST_EXPECTS(
1001 actualPaymentParts.principalPaid == 45,
1002 " principalPaid mismatch: expected 45, got `" +
1003 to_string(actualPaymentParts.principalPaid));
1004
1005 BEAST_EXPECTS(
1006 actualPaymentParts.interestPaid == 0,
1007 " interestPaid mismatch: expected 0, got " +
1008 to_string(actualPaymentParts.interestPaid));
1009
1010 // =========== VALIDATE STATE CHANGES ===========
1011 // With no Loan interest, interest outstanding should not change
1012 BEAST_EXPECTS(
1013 loanProperties.loanState.interestDue - newState.interestDue == 0,
1014 " interest change mismatch: expected 0, got " +
1015 to_string(loanProperties.loanState.interestDue - newState.interestDue));
1016
1017 // With no Loan management fee, management fee due should not change
1018 BEAST_EXPECTS(
1019 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1020 " management fee change mismatch: expected 0, got " +
1021 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1022
1023 BEAST_EXPECTS(
1024 actualPaymentParts.principalPaid ==
1025 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1026 " principalPaid mismatch: expected " +
1027 to_string(
1028 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1029 ", got " + to_string(actualPaymentParts.principalPaid));
1030 }
1031
1032 void
1034 {
1035 testcase("tryOverpayment - Loan Interest, No Overpayment Fees");
1036
1037 using namespace jtx;
1038 using namespace xrpl::detail;
1039
1040 Env const env{*this};
1041 Account const issuer{"issuer"};
1042 PrettyAsset const asset = issuer["USD"];
1043 std::int32_t const loanScale = -5;
1044 TenthBips16 const managementFeeRate{0}; // 0%
1045 TenthBips32 const loanInterestRate{10'000}; // 10%
1046 Number const loanPrincipal{1'000};
1047 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
1048 std::uint32_t const paymentsRemaining = 10;
1049 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
1050
1051 auto const overpaymentComponents = computeOverpaymentComponents(
1052 env.current()->rules(),
1053 asset,
1054 loanScale,
1055 Number{50, 0},
1056 TenthBips32(0), // no overpayment interest
1057 TenthBips32(0), // 0% overpayment fee
1058 managementFeeRate);
1059
1060 auto const loanProperties = computeLoanProperties(
1061 env.current()->rules(),
1062 asset,
1063 loanPrincipal,
1064 loanInterestRate,
1065 paymentInterval,
1066 paymentsRemaining,
1067 managementFeeRate,
1068 loanScale);
1069
1070 auto const ret = tryOverpayment(
1071 env.current()->rules(),
1072 asset,
1073 loanScale,
1074 overpaymentComponents,
1075 loanProperties.loanState,
1076 loanProperties.periodicPayment,
1077 periodicRate,
1078 paymentsRemaining,
1079 managementFeeRate,
1080 env.journal);
1081
1082 BEAST_EXPECT(ret);
1083
1084 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1085 auto const& newState = newLoanProperties.loanState;
1086
1087 // =========== VALIDATE PAYMENT PARTS ===========
1088 // with no overpayment interest portion, value change should equal
1089 // interest decrease
1090 BEAST_EXPECTS(
1091 (actualPaymentParts.valueChange == Number{-228802, -5}),
1092 " valueChange mismatch: expected " + to_string(Number{-228802, -5}) + ", got " +
1093 to_string(actualPaymentParts.valueChange));
1094
1095 // with no fee portion, fee paid should be zero
1096 BEAST_EXPECTS(
1097 actualPaymentParts.feePaid == 0,
1098 " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid));
1099
1100 BEAST_EXPECTS(
1101 actualPaymentParts.principalPaid == 50,
1102 " principalPaid mismatch: expected 50, got `" +
1103 to_string(actualPaymentParts.principalPaid));
1104
1105 // with no interest portion, interest paid should be zero
1106 BEAST_EXPECTS(
1107 actualPaymentParts.interestPaid == 0,
1108 " interestPaid mismatch: expected 0, got " +
1109 to_string(actualPaymentParts.interestPaid));
1110
1111 // =========== VALIDATE STATE CHANGES ===========
1112 BEAST_EXPECTS(
1113 actualPaymentParts.principalPaid ==
1114 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1115 " principalPaid mismatch: expected " +
1116 to_string(
1117 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1118 ", got " + to_string(actualPaymentParts.principalPaid));
1119
1120 BEAST_EXPECTS(
1121 actualPaymentParts.valueChange ==
1122 newState.interestDue - loanProperties.loanState.interestDue,
1123 " valueChange mismatch: expected " +
1124 to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
1125 to_string(actualPaymentParts.valueChange));
1126
1127 // With no Loan management fee, management fee due should not change
1128 BEAST_EXPECTS(
1129 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1130 " management fee change mismatch: expected 0, got " +
1131 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1132 }
1133
1134 void
1136 {
1137 testcase("tryOverpayment - Loan Interest, Overpayment Interest, No Fee");
1138
1139 using namespace jtx;
1140 using namespace xrpl::detail;
1141
1142 Env const env{*this};
1143 Account const issuer{"issuer"};
1144 PrettyAsset const asset = issuer["USD"];
1145 std::int32_t const loanScale = -5;
1146 TenthBips16 const managementFeeRate{0}; // 0%
1147 TenthBips32 const loanInterestRate{10'000}; // 10%
1148 Number const loanPrincipal{1'000};
1149 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
1150 std::uint32_t const paymentsRemaining = 10;
1151 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
1152
1153 auto const overpaymentComponents = computeOverpaymentComponents(
1154 env.current()->rules(),
1155 asset,
1156 loanScale,
1157 Number{50, 0},
1158 TenthBips32(10'000), // 10% overpayment interest
1159 TenthBips32(0), // 0% overpayment fee
1160 managementFeeRate);
1161
1162 auto const loanProperties = computeLoanProperties(
1163 env.current()->rules(),
1164 asset,
1165 loanPrincipal,
1166 loanInterestRate,
1167 paymentInterval,
1168 paymentsRemaining,
1169 managementFeeRate,
1170 loanScale);
1171
1172 auto const ret = tryOverpayment(
1173 env.current()->rules(),
1174 asset,
1175 loanScale,
1176 overpaymentComponents,
1177 loanProperties.loanState,
1178 loanProperties.periodicPayment,
1179 periodicRate,
1180 paymentsRemaining,
1181 managementFeeRate,
1182 env.journal);
1183
1184 BEAST_EXPECT(ret);
1185
1186 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1187 auto const& newState = newLoanProperties.loanState;
1188
1189 // =========== VALIDATE PAYMENT PARTS ===========
1190 // with overpayment interest portion, interest paid should be 5
1191 BEAST_EXPECTS(
1192 actualPaymentParts.interestPaid == 5,
1193 " interestPaid mismatch: expected 5, got " +
1194 to_string(actualPaymentParts.interestPaid));
1195
1196 // With overpayment interest portion, value change should equal the
1197 // interest decrease plus overpayment interest portion
1198 BEAST_EXPECTS(
1199 (actualPaymentParts.valueChange ==
1200 Number{-205922, -5} + actualPaymentParts.interestPaid),
1201 " valueChange mismatch: expected " +
1202 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid) +
1203 ", got " + to_string(actualPaymentParts.valueChange));
1204
1205 // with no fee portion, fee paid should be zero
1206 BEAST_EXPECTS(
1207 actualPaymentParts.feePaid == 0,
1208 " feePaid mismatch: expected 0, got " + to_string(actualPaymentParts.feePaid));
1209
1210 BEAST_EXPECTS(
1211 actualPaymentParts.principalPaid == 45,
1212 " principalPaid mismatch: expected 45, got `" +
1213 to_string(actualPaymentParts.principalPaid));
1214
1215 // =========== VALIDATE STATE CHANGES ===========
1216 BEAST_EXPECTS(
1217 actualPaymentParts.principalPaid ==
1218 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1219 " principalPaid mismatch: expected " +
1220 to_string(
1221 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1222 ", got " + to_string(actualPaymentParts.principalPaid));
1223
1224 // The change in interest is equal to the value change sans the
1225 // overpayment interest
1226 BEAST_EXPECTS(
1227 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1228 newState.interestDue - loanProperties.loanState.interestDue,
1229 " valueChange mismatch: expected " +
1230 to_string(
1231 newState.interestDue - loanProperties.loanState.interestDue +
1232 actualPaymentParts.interestPaid) +
1233 ", got " + to_string(actualPaymentParts.valueChange));
1234
1235 // With no Loan management fee, management fee due should not change
1236 BEAST_EXPECTS(
1237 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1238 " management fee change mismatch: expected 0, got " +
1239 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1240 }
1241
1242 void
1244 {
1245 testcase(
1246 "tryOverpayment - Loan Interest and Fee, Overpayment Interest, No "
1247 "Fee");
1248
1249 using namespace jtx;
1250 using namespace xrpl::detail;
1251
1252 Env const env{*this};
1253 Account const issuer{"issuer"};
1254 PrettyAsset const asset = issuer["USD"];
1255 std::int32_t const loanScale = -5;
1256 TenthBips16 const managementFeeRate{10'000}; // 10%
1257 TenthBips32 const loanInterestRate{10'000}; // 10%
1258 Number const loanPrincipal{1'000};
1259 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
1260 std::uint32_t const paymentsRemaining = 10;
1261 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
1262
1263 auto const overpaymentComponents = computeOverpaymentComponents(
1264 env.current()->rules(),
1265 asset,
1266 loanScale,
1267 Number{50, 0},
1268 TenthBips32(10'000), // 10% overpayment interest
1269 TenthBips32(0), // 0% overpayment fee
1270 managementFeeRate);
1271
1272 auto const loanProperties = computeLoanProperties(
1273 env.current()->rules(),
1274 asset,
1275 loanPrincipal,
1276 loanInterestRate,
1277 paymentInterval,
1278 paymentsRemaining,
1279 managementFeeRate,
1280 loanScale);
1281
1282 auto const ret = tryOverpayment(
1283 env.current()->rules(),
1284 asset,
1285 loanScale,
1286 overpaymentComponents,
1287 loanProperties.loanState,
1288 loanProperties.periodicPayment,
1289 periodicRate,
1290 paymentsRemaining,
1291 managementFeeRate,
1292 env.journal);
1293
1294 BEAST_EXPECT(ret);
1295
1296 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1297 auto const& newState = newLoanProperties.loanState;
1298
1299 // =========== VALIDATE PAYMENT PARTS ===========
1300
1301 // Since there is loan management fee, the fee is charged against
1302 // overpayment interest portion first, so interest paid remains 4.5
1303 BEAST_EXPECTS(
1304 (actualPaymentParts.interestPaid == Number{45, -1}),
1305 " interestPaid mismatch: expected 4.5, got " +
1306 to_string(actualPaymentParts.interestPaid));
1307
1308 // With overpayment interest portion, value change should equal the
1309 // interest decrease plus overpayment interest portion
1310 BEAST_EXPECTS(
1311 (actualPaymentParts.valueChange ==
1312 Number{-18533, -4} + actualPaymentParts.interestPaid),
1313 " valueChange mismatch: expected " +
1314 to_string(Number{-18533, -4} + actualPaymentParts.interestPaid) + ", got " +
1315 to_string(actualPaymentParts.valueChange));
1316
1317 // While there is no overpayment fee, fee paid should equal the
1318 // management fee charged against the overpayment interest portion
1319 BEAST_EXPECTS(
1320 (actualPaymentParts.feePaid == Number{5, -1}),
1321 " feePaid mismatch: expected 0.5, got " + to_string(actualPaymentParts.feePaid));
1322
1323 BEAST_EXPECTS(
1324 actualPaymentParts.principalPaid == 45,
1325 " principalPaid mismatch: expected 45, got `" +
1326 to_string(actualPaymentParts.principalPaid));
1327
1328 // =========== VALIDATE STATE CHANGES ===========
1329 BEAST_EXPECTS(
1330 actualPaymentParts.principalPaid ==
1331 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1332 " principalPaid mismatch: expected " +
1333 to_string(
1334 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1335 ", got " + to_string(actualPaymentParts.principalPaid));
1336
1337 // Note that the management fee value change is not captured, as this
1338 // value is not needed to correctly update the Vault state.
1339 BEAST_EXPECTS(
1340 (newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
1341 Number{-20592, -5}),
1342 " management fee change mismatch: expected " + to_string(Number{-20592, -5}) +
1343 ", got " +
1344 to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
1345
1346 BEAST_EXPECTS(
1347 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1348 newState.interestDue - loanProperties.loanState.interestDue,
1349 " valueChange mismatch: expected " +
1350 to_string(newState.interestDue - loanProperties.loanState.interestDue) + ", got " +
1351 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
1352 }
1353
1354 void
1356 {
1357 testcase("tryOverpayment - Loan Interest, Fee, Overpayment Interest, Fee");
1358
1359 using namespace jtx;
1360 using namespace xrpl::detail;
1361
1362 Account const issuer{"issuer"};
1363 PrettyAsset const asset = issuer["USD"];
1364 std::int32_t const loanScale = -5;
1365 TenthBips16 const managementFeeRate{10'000}; // 10%
1366 TenthBips32 const loanInterestRate{10'000}; // 10%
1367 Number const loanPrincipal{1'000};
1368 std::uint32_t const paymentInterval = 30 * 24 * 60 * 60;
1369 std::uint32_t const paymentsRemaining = 10;
1370 auto const periodicRate = loanPeriodicRate(loanInterestRate, paymentInterval);
1371
1372 Env const env{*this};
1373 auto const overpaymentComponents = computeOverpaymentComponents(
1374 env.current()->rules(),
1375 asset,
1376 loanScale,
1377 Number{50, 0},
1378 TenthBips32(10'000), // 10% overpayment interest
1379 TenthBips32(10'000), // 10% overpayment fee
1380 managementFeeRate);
1381
1382 struct Outcome
1383 {
1384 LoanPaymentParts parts;
1385 LoanState oldState;
1386 LoanState newState;
1387 };
1388
1389 // Run tryOverpayment under a given amendment set. At this (non-near-zero)
1390 // rate computeLoanProperties is amendment-independent, so the loan state
1391 // is identical across the amendment; only tryOverpayment's fixCleanup3_2_0
1392 // behaviour (the exact-principal pin and the management-fee re-derivation
1393 // from that principal) differs.
1394 auto run = [&](FeatureBitset features) -> std::optional<Outcome> {
1395 Env const env{*this, features};
1396 auto const loanProperties = computeLoanProperties(
1397 env.current()->rules(),
1398 asset,
1399 loanPrincipal,
1400 loanInterestRate,
1401 paymentInterval,
1402 paymentsRemaining,
1403 managementFeeRate,
1404 loanScale);
1405 auto const ret = tryOverpayment(
1406 env.current()->rules(),
1407 asset,
1408 loanScale,
1409 overpaymentComponents,
1410 loanProperties.loanState,
1411 loanProperties.periodicPayment,
1412 periodicRate,
1413 paymentsRemaining,
1414 managementFeeRate,
1415 env.journal);
1416 if (!BEAST_EXPECT(ret))
1417 return std::nullopt;
1418 return Outcome{
1419 .parts = ret->first,
1420 .oldState = loanProperties.loanState,
1421 .newState = ret->second.loanState};
1422 };
1423
1424 auto const fixedOpt = run(testableAmendments());
1425 auto const legacyOpt = run(testableAmendments() - fixCleanup3_2_0);
1426 if (!fixedOpt || !legacyOpt)
1427 {
1428 BEAST_EXPECT(fixedOpt.has_value());
1429 BEAST_EXPECT(legacyOpt.has_value());
1430 return;
1431 }
1432 Outcome const& fixed = *fixedOpt;
1433 Outcome const& legacy = *legacyOpt;
1434
1435 // Components that the amendment does not change. The management fee is
1436 // charged against the overpayment interest portion first, so interest
1437 // paid stays 4.5 and fee paid 5.5; the principal repaid is 40 in both.
1438 auto checkCommon = [&](Outcome const& o, char const* tag) {
1439 BEAST_EXPECTS(
1440 (o.parts.interestPaid == Number{45, -1}),
1441 std::string(tag) + " interestPaid " + to_string(o.parts.interestPaid));
1442 BEAST_EXPECTS(
1443 (o.parts.feePaid == Number{55, -1}),
1444 std::string(tag) + " feePaid " + to_string(o.parts.feePaid));
1445 BEAST_EXPECTS(
1446 o.parts.principalPaid == 40,
1447 std::string(tag) + " principalPaid " + to_string(o.parts.principalPaid));
1448 BEAST_EXPECT(
1449 o.parts.principalPaid ==
1450 o.oldState.principalOutstanding - o.newState.principalOutstanding);
1451 // v = p + i + m identity: the non-interest part of valueChange equals
1452 // the interest-due change.
1453 BEAST_EXPECT(
1454 o.parts.valueChange - o.parts.interestPaid ==
1455 o.newState.interestDue - o.oldState.interestDue);
1456 };
1457 checkCommon(fixed, "fixed");
1458 checkCommon(legacy, "legacy");
1459
1460 // With fixCleanup3_2_0 the management fee is re-derived from the exact
1461 // principal; without it, from the one-scale-unit-high round-trip
1462 // principal. So the management fee outstanding (and hence the value
1463 // change, via v = p + i + m) differ by exactly one scale-unit (1e-5 at
1464 // loanScale -5) between the two paths.
1465 BEAST_EXPECT((fixed.parts.valueChange == Number{-164738, -5} + fixed.parts.interestPaid));
1466 BEAST_EXPECT(
1467 (fixed.newState.managementFeeDue - fixed.oldState.managementFeeDue ==
1468 Number{-18303, -5}));
1469 BEAST_EXPECT((legacy.parts.valueChange == Number{-164737, -5} + legacy.parts.interestPaid));
1470 BEAST_EXPECT(
1471 (legacy.newState.managementFeeDue - legacy.oldState.managementFeeDue ==
1472 Number{-18304, -5}));
1473 }
1474
1475 void
1477 {
1478 using namespace xrpl::accrual;
1479
1480 struct TestCase
1481 {
1482 std::string name;
1483 Number principalRequested;
1484 Number interestDue;
1485 };
1486
1487 auto const testCases = std::vector<TestCase>{
1488 {.name = "Zero interest",
1489 .principalRequested = Number{1'000},
1490 .interestDue = Number{0}},
1491 {.name = "Nonzero interest",
1492 .principalRequested = Number{1'000},
1493 .interestDue = Number{75}},
1494 };
1495
1496 for (auto const& tc : testCases)
1497 {
1498 testcase("accrual::loanOriginationDeltas: " + tc.name);
1499
1500 auto const deltas = loanOriginationDeltas(tc.principalRequested, tc.interestDue);
1501 BEAST_EXPECTS(
1502 deltas.assetsTotalDelta == tc.interestDue,
1503 "assetsTotalDelta mismatch: expected " + to_string(tc.interestDue) + ", got " +
1504 to_string(deltas.assetsTotalDelta));
1505 BEAST_EXPECTS(
1506 deltas.debtTotalDelta == tc.principalRequested + tc.interestDue,
1507 "debtTotalDelta mismatch: expected " +
1508 to_string(tc.principalRequested + tc.interestDue) + ", got " +
1509 to_string(deltas.debtTotalDelta));
1510 }
1511 }
1512
1513 void
1515 {
1516 using namespace xrpl::cash_basis;
1517
1518 testcase("cash_basis::loanOriginationDeltas: interestDue is ignored");
1519
1520 Number const principalRequested{1'000};
1521 Number const interestDue{75};
1522
1523 auto const deltas = loanOriginationDeltas(principalRequested);
1524 BEAST_EXPECTS(
1525 deltas.assetsTotalDelta == 0,
1526 "assetsTotalDelta mismatch: expected 0, got " + to_string(deltas.assetsTotalDelta));
1527 BEAST_EXPECTS(
1528 deltas.debtTotalDelta == principalRequested,
1529 "debtTotalDelta mismatch: expected " + to_string(principalRequested) + ", got " +
1530 to_string(deltas.debtTotalDelta));
1531 }
1532
1533 void
1535 {
1536 using namespace xrpl::accrual;
1537
1538 struct TestCase
1539 {
1540 std::string name;
1541 Number vaultMaximum;
1542 Number vaultTotal;
1543 Number interestDue;
1544 bool expected;
1545 };
1546
1547 auto const testCases = std::vector<TestCase>{
1548 {.name = "No maximum configured",
1549 .vaultMaximum = Number{0},
1550 .vaultTotal = Number{900},
1551 .interestDue = Number{1'000},
1552 .expected = false},
1553 {.name = "Interest fits under headroom",
1554 .vaultMaximum = Number{1'000},
1555 .vaultTotal = Number{900},
1556 .interestDue = Number{50},
1557 .expected = false},
1558 {.name = "Interest exactly fills headroom",
1559 .vaultMaximum = Number{1'000},
1560 .vaultTotal = Number{900},
1561 .interestDue = Number{100},
1562 .expected = false},
1563 {.name = "Interest exceeds headroom",
1564 .vaultMaximum = Number{1'000},
1565 .vaultTotal = Number{900},
1566 .interestDue = Number{101},
1567 .expected = true},
1568 };
1569
1570 for (auto const& tc : testCases)
1571 {
1572 testcase("accrual::loanOriginationExceedsVaultMaximum: " + tc.name);
1573 BEAST_EXPECT(
1575 tc.vaultMaximum, tc.vaultTotal, tc.interestDue) == tc.expected);
1576 }
1577 }
1578
1579 // Constructs a minimal ltLOAN SLE with just the fields needed by
1580 // loanVaultExposure. Mirrors the bare-SLE pattern used by
1581 // testCanApplyToBrokerCover for ltLOAN_BROKER.
1584 Number const& totalValueOutstanding,
1585 Number const& principalOutstanding,
1586 Number const& managementFeeOutstanding)
1587 {
1588 auto sle = std::make_shared<SLE>(ltLOAN, uint256{1u});
1589 sle->at(sfTotalValueOutstanding) = totalValueOutstanding;
1590 sle->at(sfPrincipalOutstanding) = principalOutstanding;
1591 sle->at(sfManagementFeeOutstanding) = managementFeeOutstanding;
1592 return sle;
1593 }
1594
1595 // Constructs a minimal ltVAULT SLE with just LEVersion set (or left
1596 // absent), for exercising the dispatchers' per-Vault gating.
1599 std::optional<VaultVersion> leVersion = std::nullopt,
1600 std::optional<Number> assetsMaximum = std::nullopt,
1601 std::optional<Number> assetsTotal = std::nullopt)
1602 {
1603 auto sle = std::make_shared<SLE>(ltVAULT, uint256{2u});
1604 if (leVersion)
1605 sle->at(sfLEVersion) = std::to_underlying(*leVersion);
1606 if (assetsMaximum)
1607 sle->at(sfAssetsMaximum) = *assetsMaximum;
1608 if (assetsTotal)
1609 sle->at(sfAssetsTotal) = *assetsTotal;
1610 return sle;
1611 }
1612
1613 void
1615 {
1616 testcase("accrual::loanVaultExposure");
1617
1618 auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50});
1619 BEAST_EXPECT(xrpl::accrual::loanVaultExposure(sle) == Number{950});
1620 }
1621
1622 void
1624 {
1625 testcase("cash_basis::loanVaultExposure");
1626
1627 auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50});
1628 BEAST_EXPECT(xrpl::cash_basis::loanVaultExposure(sle) == Number{800});
1629 }
1630
1631 void
1633 {
1634 // principalPaid, interestPaid, feePaid, valueChange are all distinct
1635 // and nonzero, with a nonzero valueChange simulating a late-payment
1636 // penalty, so Accrual's formula is meaningfully exercised.
1637 LoanPaymentParts const parts{
1638 .principalPaid = Number{100},
1639 .interestPaid = Number{20},
1640 .valueChange = Number{5},
1641 .feePaid = Number{3}};
1642
1643 {
1644 testcase("accrual::loanPaymentDeltas: nonzero valueChange");
1645 auto const deltas = xrpl::accrual::loanPaymentDeltas(parts);
1646 BEAST_EXPECT(deltas.assetsTotalDelta == parts.valueChange);
1647 BEAST_EXPECT(
1648 deltas.debtTotalDelta ==
1649 (parts.principalPaid + parts.interestPaid) - parts.valueChange);
1650 }
1651
1652 {
1653 testcase("cash_basis::loanPaymentDeltas: nonzero valueChange ignored");
1654 auto const deltas = xrpl::cash_basis::loanPaymentDeltas(parts);
1655 BEAST_EXPECT(deltas.assetsTotalDelta == parts.interestPaid);
1656 BEAST_EXPECT(deltas.debtTotalDelta == parts.principalPaid);
1657 }
1658 }
1659
1660 void
1662 {
1663 using namespace jtx;
1664
1665 Number const principalRequested{1'000};
1666 Number const interestDue{75};
1667
1668 auto const legacyVault = makeVaultSle();
1669 auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis);
1670
1671 {
1672 testcase(
1673 "loanOriginationDeltas dispatcher: amendment enabled, legacy vault picks "
1674 "Accrual");
1675 Env const env{*this};
1676 auto const deltas = loanOriginationDeltas(legacyVault, principalRequested, interestDue);
1677 auto const expected =
1678 xrpl::accrual::loanOriginationDeltas(principalRequested, interestDue);
1679 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1680 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1681 }
1682
1683 {
1684 testcase(
1685 "loanOriginationDeltas dispatcher: amendment enabled, LEVersion == "
1686 "VaultVersion::CashBasis picks CashBasis");
1687 Env const env{*this};
1688 auto const deltas =
1689 loanOriginationDeltas(cashBasisVault, principalRequested, interestDue);
1690 auto const expected = xrpl::cash_basis::loanOriginationDeltas(principalRequested);
1691 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1692 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1693 }
1694 }
1695
1696 void
1698 {
1699 using namespace jtx;
1700
1701 Number const vaultMaximum{1'000};
1702 Number const vaultTotal{900};
1703 // Exceeds Accrual's headroom (100), but must never trip CashBasis.
1704 Number const interestDue{101};
1705
1706 auto const legacyVault = makeVaultSle(std::nullopt, vaultMaximum, vaultTotal);
1707 auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis, vaultMaximum, vaultTotal);
1708
1709 {
1710 testcase(
1711 "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, legacy vault "
1712 "picks Accrual");
1713 Env const env{*this};
1714 BEAST_EXPECT(
1715 loanOriginationExceedsVaultMaximum(legacyVault, vaultTotal, interestDue) ==
1717 vaultMaximum, vaultTotal, interestDue));
1718 }
1719
1720 {
1721 testcase(
1722 "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, LEVersion == "
1723 "VaultVersion::CashBasis picks CashBasis");
1724 Env const env{*this};
1725 BEAST_EXPECT(
1726 loanOriginationExceedsVaultMaximum(cashBasisVault, vaultTotal, interestDue) ==
1727 false);
1728 }
1729 }
1730
1731 void
1733 {
1734 using namespace jtx;
1735
1736 auto const legacyVault = makeVaultSle();
1737 auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis);
1738
1739 {
1740 testcase("loanVaultExposure dispatcher: amendment enabled, legacy vault picks Accrual");
1741 Env const env{*this};
1742 auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50});
1743 BEAST_EXPECT(
1744 loanVaultExposure(legacyVault, sle) == xrpl::accrual::loanVaultExposure(sle));
1745 }
1746
1747 {
1748 testcase(
1749 "loanVaultExposure dispatcher: amendment enabled, LEVersion == "
1750 "VaultVersion::CashBasis "
1751 "picks CashBasis");
1752 Env const env{*this};
1753 auto sle = makeLoanSle(Number{1'000}, Number{800}, Number{50});
1754 BEAST_EXPECT(
1755 loanVaultExposure(cashBasisVault, sle) == xrpl::cash_basis::loanVaultExposure(sle));
1756 }
1757 }
1758
1759 void
1761 {
1762 using namespace jtx;
1763
1764 LoanPaymentParts const parts{
1765 .principalPaid = Number{100},
1766 .interestPaid = Number{20},
1767 .valueChange = Number{5},
1768 .feePaid = Number{3}};
1769
1770 auto const legacyVault = makeVaultSle();
1771 auto const cashBasisVault = makeVaultSle(VaultVersion::CashBasis);
1772
1773 {
1774 testcase("loanPaymentDeltas dispatcher: amendment enabled, legacy vault picks Accrual");
1775 Env const env{*this};
1776 auto const deltas = loanPaymentDeltas(legacyVault, parts);
1777 auto const expected = xrpl::accrual::loanPaymentDeltas(parts);
1778 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1779 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1780 }
1781
1782 {
1783 testcase(
1784 "loanPaymentDeltas dispatcher: amendment enabled, LEVersion == "
1785 "VaultVersion::CashBasis "
1786 "picks CashBasis");
1787 Env const env{*this};
1788 auto const deltas = loanPaymentDeltas(cashBasisVault, parts);
1789 auto const expected = xrpl::cash_basis::loanPaymentDeltas(parts);
1790 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1791 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1792 }
1793 }
1794
1795public:
1796 void
1798 {
1799 using namespace jtx;
1800
1801 Account const issuer{"issuer"};
1802 PrettyAsset const iou = issuer["IOU"];
1803
1804 // sfCoverAvailable = Number{10} on an IOU → STAmount exponent = -14,
1805 // so coverScale = -14. The ULP boundary is 5e-15; anything below
1806 // that rounds to zero at cover scale. Number{1,-16} = 1e-16 is our
1807 // representative sub-ULP probe.
1808 struct TestCase
1809 {
1810 std::string name;
1811 Number coverAvailable;
1812 STAmount amount;
1813 TER expected;
1814 };
1815
1816 auto const testCases = std::vector<TestCase>{
1817 {
1818 .name = "Zero amount",
1819 .coverAvailable = Number{10},
1820 .amount = STAmount{iou, Number{0}},
1821 .expected = tecPRECISION_LOSS,
1822 },
1823 {
1824 .name = "Rounds to zero at cover scale",
1825 .coverAvailable = Number{10},
1826 .amount = STAmount{iou, Number{1, -16}},
1827 .expected = tecPRECISION_LOSS,
1828 },
1829 {
1830 .name = "Zero coverAvailable, whole-unit amount",
1831 // coverScale = 0 (zero STAmount exponent); 1 IOU is not
1832 // zero at integer scale → tesSUCCESS.
1833 .coverAvailable = Number{0},
1834 .amount = STAmount{iou, Number{1}},
1835 .expected = tesSUCCESS,
1836 },
1837 {
1838 .name = "Supra-ULP amount",
1839 .coverAvailable = Number{10},
1840 .amount = STAmount{iou, Number{1, -13}},
1841 .expected = tesSUCCESS,
1842 },
1843 };
1844
1845 Env const env{*this};
1846
1847 for (auto const& tc : testCases)
1848 {
1849 testcase("canApplyToBrokerCover: " + tc.name);
1850 auto sle = std::make_shared<SLE>(ltLOAN_BROKER, uint256{1u});
1851 sle->at(sfCoverAvailable) = tc.coverAvailable;
1852 BEAST_EXPECT(
1853 canApplyToBrokerCover(*env.current(), sle, iou, tc.amount, env.journal, "test") ==
1854 tc.expected);
1855 }
1856
1857 // Amendment off → guard is bypassed regardless of amount.
1858 {
1859 testcase("canApplyToBrokerCover: amendment disabled");
1860 Env const envOff{*this, testableAmendments() - fixCleanup3_2_0};
1861 auto sle = std::make_shared<SLE>(ltLOAN_BROKER, uint256{1u});
1862 sle->at(sfCoverAvailable) = Number{10};
1863 BEAST_EXPECT(
1865 *envOff.current(),
1866 sle,
1867 iou,
1868 STAmount{iou, Number{0}},
1869 envOff.journal,
1870 "test") == tesSUCCESS);
1871 }
1872 }
1873
1874 void
1910};
1911
1912BEAST_DEFINE_TESTSUITE(LendingHelpers, app, xrpl);
1913
1914} // namespace xrpl::test
A testsuite class.
Definition suite.h:52
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static std::shared_ptr< SLE > makeVaultSle(std::optional< VaultVersion > leVersion=std::nullopt, std::optional< Number > assetsMaximum=std::nullopt, std::optional< Number > assetsTotal=std::nullopt)
static std::shared_ptr< SLE > makeLoanSle(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
void run() override
Runs the suite.
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
A transaction testing environment.
Definition Env.h:161
beast::Journal const journal
Definition Env.h:204
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
T make_shared(T... args)
AccountingDeltas loanPaymentDeltas(LoanPaymentParts const &parts)
bool loanOriginationExceedsVaultMaximum(Number const &vaultMaximum, Number const &vaultTotal, Number const &interestDue)
Number loanVaultExposure(SLE::const_ref loanSle)
AccountingDeltas loanOriginationDeltas(Number const &principalRequested, Number const &interestDue)
AccountingDeltas loanPaymentDeltas(LoanPaymentParts const &parts)
AccountingDeltas loanOriginationDeltas(Number const &principalRequested)
Number loanVaultExposure(SLE::const_ref loanSle)
Number computePaymentFactor(Rules const &rules, Number const &periodicRate, std::uint32_t paymentsRemaining)
Number loanPrincipalFromPeriodicPayment(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentsRemaining)
Number computePowerMinusOneHybrid(Number const &periodicRate, std::uint32_t paymentsRemaining)
Number loanPeriodicPayment(Rules const &rules, Number const &principalOutstanding, Number const &periodicRate, std::uint32_t paymentsRemaining)
Number loanAccruedInterest(Number const &principalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t startDate, std::uint32_t prevPaymentDate, std::uint32_t paymentInterval)
std::pair< Number, Number > computeInterestAndFeeParts(Asset const &asset, Number const &interest, TenthBips16 managementFeeRate, std::int32_t loanScale)
Number loanLatePaymentInterest(Number const &principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, std::uint32_t nextPaymentDueDate)
std::expected< std::pair< LoanPaymentParts, LoanProperties >, TER > tryOverpayment(Rules const &rules, Asset const &asset, std::int32_t loanScale, ExtendedPaymentComponents const &overpaymentComponents, LoanState const &roundedLoanState, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentRemaining, TenthBips16 const managementFeeRate, beast::Journal j)
ExtendedPaymentComponents computeOverpaymentComponents(Rules const &rules, Asset const &asset, int32_t const loanScale, Number const &overpayment, TenthBips32 const overpaymentInterestRate, TenthBips32 const overpaymentFeeRate, TenthBips16 const managementFeeRate)
Number computePowerMinusOne(Number const &periodicRate, std::uint32_t paymentsRemaining)
FeatureBitset testableAmendments()
Definition Env.h:92
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
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)
TER canApplyToBrokerCover(ReadView const &view, SLE::const_ref sleBroker, Asset const &vaultAsset, STAmount const &amount, beast::Journal j, std::string_view logPrefix)
Broker cover preclaim precision guard (fixCleanup3_2_0).
bool loanOriginationExceedsVaultMaximum(SLE::const_ref vaultSle, Number const &vaultTotal, Number const &interestDue)
AccountingDeltas loanPaymentDeltas(SLE::const_ref vaultSle, LoanPaymentParts const &parts)
Number power(Number const &f, unsigned n)
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
static FunctionType fixed(Keylet const &keylet)
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
constexpr Number abs(Number x) noexcept
Definition Number.h:876
AccountingDeltas loanOriginationDeltas(SLE::const_ref vaultSle, Number const &principalRequested, Number const &interestDue)
TERSubset< CanCvtToTER > TER
Definition TER.h:647
Number loanVaultExposure(SLE::const_ref vaultSle, SLE::const_ref loanSle)
@ tecPRECISION_LOSS
Definition TER.h:366
LoanProperties computeLoanProperties(Rules const &rules, Asset const &asset, Number const &principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
BaseUInt< 256 > uint256
Definition base_uint.h:580
@ tesSUCCESS
Definition TER.h:245
Number computeFullPaymentInterest(Number const &theoreticalPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)
This structure captures the parts of a loan state.
T to_string(T... args)