1#include <xrpl/beast/unit_test/suite.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/amount.h>
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/SField.h>
13#include <xrpl/protocol/STAmount.h>
14#include <xrpl/protocol/STLedgerEntry.h>
15#include <xrpl/protocol/TER.h>
16#include <xrpl/protocol/Units.h>
34 auto const& rules = env.
current()->rules();
40 Number expectedPaymentFactor;
45 .name =
"Zero periodic rate",
47 .paymentsRemaining = 4,
48 .expectedPaymentFactor =
Number{25, -2},
51 .name =
"One payment remaining",
52 .periodicRate =
Number{5, -2},
53 .paymentsRemaining = 1,
54 .expectedPaymentFactor =
Number{105, -2},
57 .name =
"Multiple payments remaining",
58 .periodicRate =
Number{5, -2},
59 .paymentsRemaining = 3,
60 .expectedPaymentFactor =
Number{3672085646312450436, -19},
63 .name =
"Zero payments remaining",
64 .periodicRate =
Number{5, -2},
65 .paymentsRemaining = 0,
66 .expectedPaymentFactor =
Number{0},
70 for (
auto const& tc : testCases)
72 testcase(
"computePaymentFactor: " + tc.name);
74 auto const computedPaymentFactor =
77 computedPaymentFactor == tc.expectedPaymentFactor,
78 "Payment factor mismatch: expected " +
to_string(tc.expectedPaymentFactor) +
79 ", got " +
to_string(computedPaymentFactor));
89 auto const& rules = env.
current()->rules();
94 Number principalOutstanding;
97 Number expectedPeriodicPayment;
102 .name =
"Zero principal outstanding",
103 .principalOutstanding =
Number{0},
104 .periodicRate =
Number{5, -2},
105 .paymentsRemaining = 5,
106 .expectedPeriodicPayment =
Number{0},
109 .name =
"Zero payments remaining",
110 .principalOutstanding =
Number{1'000},
111 .periodicRate =
Number{5, -2},
112 .paymentsRemaining = 0,
113 .expectedPeriodicPayment =
Number{0},
116 .name =
"Zero periodic rate",
117 .principalOutstanding =
Number{1'000},
118 .periodicRate =
Number{0},
119 .paymentsRemaining = 4,
120 .expectedPeriodicPayment =
Number{250},
123 .name =
"Standard case",
124 .principalOutstanding =
Number{1'000},
126 .paymentsRemaining = 3,
127 .expectedPeriodicPayment =
Number{389569066396123265, -15},
131 for (
auto const& tc : testCases)
133 testcase(
"loanPeriodicPayment: " + tc.name);
136 rules, tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
138 computedPeriodicPayment == tc.expectedPeriodicPayment,
139 "Periodic payment mismatch: expected " +
to_string(tc.expectedPeriodicPayment) +
140 ", got " +
to_string(computedPeriodicPayment));
149 Env const env{*
this};
150 auto const& rules = env.
current()->rules();
158 Number expectedPrincipalOutstanding;
163 .name =
"Zero periodic payment",
164 .periodicPayment =
Number{0},
165 .periodicRate =
Number{5, -2},
166 .paymentsRemaining = 5,
167 .expectedPrincipalOutstanding =
Number{0},
170 .name =
"Zero payments remaining",
171 .periodicPayment =
Number{1'000},
172 .periodicRate =
Number{5, -2},
173 .paymentsRemaining = 0,
174 .expectedPrincipalOutstanding =
Number{0},
177 .name =
"Zero periodic rate",
178 .periodicPayment =
Number{250},
179 .periodicRate =
Number{0},
180 .paymentsRemaining = 4,
181 .expectedPrincipalOutstanding =
Number{1'000},
184 .name =
"Standard case",
185 .periodicPayment =
Number{389569066396123265, -15},
187 .paymentsRemaining = 3,
188 .expectedPrincipalOutstanding =
Number{1'000},
192 for (
auto const& tc : testCases)
194 testcase(
"loanPrincipalFromPeriodicPayment: " + tc.name);
197 rules, tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
199 computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
200 "Principal outstanding mismatch: expected " +
201 to_string(tc.expectedPrincipalOutstanding) +
", got " +
202 to_string(computedPrincipalOutstanding));
214 testcase(
"computePowerMinusOne: zero rate returns zero");
218 testcase(
"computePowerMinusOne: zero paymentsRemaining returns zero");
219 Number const fivePercent{5, -2};
224 testcase(
"computePowerMinusOne: standard case (1.05)^3 - 1 = 0.157625");
226 Number const expected{157625, -6};
231 testcase(
"computePowerMinusOne: r=1, n=1");
241 testcase(
"computePowerMinusOne: near-zero rate matches independent 2r + r^2");
245 Number const independentExpected = 2 * r + r * r;
250 testcase(
"computePowerMinusOne: near-zero rate matches independent 3r + 3r^2 + r^3");
252 Number const independentExpected = 3 * r + 3 * r * r + r * r * r;
263 testcase(
"computePowerMinusOne: large n, early termination matches hybrid output");
288 testcase(
"computePowerMinusOneHybrid: r*n >= 1e-9 uses closed form (bit-exact match)");
290 struct AboveThreshold
297 {.name =
"r=5%, n=3", .r =
Number{5, -2}, .n = 3},
298 {.name =
"r=0.1%, n=1000", .r =
Number{1, -3}, .n = 1'000},
299 {.name =
"r=1e-7, n=100 (above threshold by 10x)", .r =
Number{1, -7}, .n = 100},
301 for (
auto const& tc : cases)
317 "computePowerMinusOneHybrid: r*n < 1e-9 uses binomial expansion (bit-exact match)");
319 struct BelowThreshold
328 {.name =
"bug regime: r~1.9e-10, n=2",
331 {.name =
"r=1e-12, n=100", .r =
Number{1, -12}, .n = 100},
333 for (
auto const& tc : cases)
345 testcase(
"computePowerMinusOneHybrid: edge cases");
346 Number const fivePercent{5, -2};
358 testcase(
"computePowerMinusOneHybrid: threshold boundary r*n = 1e-9");
368 {.name =
"r=1e-9, n=1", .r =
Number{1, -9}, .n = 1},
369 {.name =
"r=1e-12, n=1000", .r =
Number{1, -12}, .n = 1'000},
372 for (
auto const& tc : cases)
382 tc.name +
": hybrid should equal closed at threshold; got hybrid=" +
388 Number const tolerance{1, -18};
392 tc.name +
": closed and binomial diverge at threshold by " +
to_string(diff));
404 testcase(
"loanPrincipalFromPeriodicPayment: principal <= payment*n at near-zero rate");
407 Env const env{*
this};
408 auto const& rules = env.
current()->rules();
417 for (
auto const n : {3u, 2u, 1u})
419 auto const computed =
421 auto const upperBound = periodicPayment *
Number{n};
423 computed <= upperBound,
436 testcase(
"computeTheoreticalLoanState: non-negative interestDue at near-zero rate");
439 Env const env{*
this};
440 auto const& rules = env.
current()->rules();
448 BEAST_EXPECT(state.principalOutstanding <= state.valueOutstanding);
449 BEAST_EXPECT(state.interestDue >= 0);
450 BEAST_EXPECT(state.managementFeeDue == 0);
461 testcase(
"computePaymentFactor: near-zero rate, amendment disabled vs enabled");
473 Number const reference = (1 + 3 * r + 3 * r * r + r * r * r) / (3 + 3 * r + r * r);
481 Env const envFix{*
this};
485 BEAST_EXPECT(buggyFactor != correctFactor);
489 BEAST_EXPECT(
abs(correctFactor - reference) <
Number(1, -15));
493 BEAST_EXPECT(
abs(buggyFactor - reference) >
Number(1, -12));
499 testcase(
"computeOverpaymentComponents");
503 Account const issuer{
"issuer"};
505 int32_t
const loanScale = 1;
506 auto const overpayment =
Number{1'000};
507 auto const overpaymentInterestRate =
TenthBips32{10'000};
508 auto const overpaymentFeeRate =
TenthBips32{50'000};
509 auto const managementFeeRate =
TenthBips16{10'000};
511 auto const expectedOverpaymentFee =
Number{500};
512 auto const expectedOverpaymentInterestGross =
Number{100};
513 auto const expectedOverpaymentInterestNet =
Number{90};
514 auto const expectedOverpaymentManagementFee =
Number{10};
515 auto const expectedPrincipalPortion =
Number{400};
517 Env const env{*
this};
523 overpaymentInterestRate,
527 BEAST_EXPECT(components.untrackedManagementFee == expectedOverpaymentFee);
529 BEAST_EXPECT(components.untrackedInterest == expectedOverpaymentInterestNet);
531 BEAST_EXPECT(components.trackedInterestPart() == expectedOverpaymentInterestNet);
533 BEAST_EXPECT(components.trackedManagementFeeDelta == expectedOverpaymentManagementFee);
534 BEAST_EXPECT(components.trackedPrincipalDelta == expectedPrincipalPortion);
536 components.trackedManagementFeeDelta + components.untrackedInterest ==
537 expectedOverpaymentInterestGross);
540 components.trackedManagementFeeDelta + components.untrackedInterest +
541 components.trackedPrincipalDelta + components.untrackedManagementFee ==
556 Number expectedInterestPart;
560 Account const issuer{
"issuer"};
565 {.name =
"Zero interest",
568 .expectedInterestPart =
Number{0},
569 .expectedFeePart =
Number{0}},
570 {.name =
"Zero fee rate",
571 .interest =
Number{1'000},
573 .expectedInterestPart =
Number{1'000},
574 .expectedFeePart =
Number{0}},
575 {.name =
"10% fee rate",
576 .interest =
Number{1'000},
578 .expectedInterestPart =
Number{900},
579 .expectedFeePart =
Number{100}},
582 for (
auto const& tc : testCases)
584 testcase(
"computeInterestAndFeeParts: " + tc.name);
586 auto const [computedInterestPart, computedFeePart] =
589 computedInterestPart == tc.expectedInterestPart,
590 "Interest part mismatch: expected " +
to_string(tc.expectedInterestPart) +
591 ", got " +
to_string(computedInterestPart));
593 computedFeePart == tc.expectedFeePart,
594 "Fee part mismatch: expected " +
to_string(tc.expectedFeePart) +
", got " +
607 Number principalOutstanding;
611 Number expectedLateInterest;
616 .name =
"On-time payment",
617 .principalOutstanding =
Number{1'000},
620 .nextPaymentDueDate = 3'000,
621 .expectedLateInterest =
Number{0},
624 .name =
"Early payment",
625 .principalOutstanding =
Number{1'000},
628 .nextPaymentDueDate = 4'000,
629 .expectedLateInterest =
Number{0},
632 .name =
"No principal outstanding",
633 .principalOutstanding =
Number{0},
636 .nextPaymentDueDate = 2'000,
637 .expectedLateInterest =
Number{0},
640 .name =
"No late interest rate",
641 .principalOutstanding =
Number{1'000},
644 .nextPaymentDueDate = 2'000,
645 .expectedLateInterest =
Number{0},
648 .name =
"Late payment",
649 .principalOutstanding =
Number{1'000},
652 .nextPaymentDueDate = 2'000,
653 .expectedLateInterest =
Number{317097919837645865, -19},
657 for (
auto const& tc : testCases)
659 testcase(
"loanLatePaymentInterest: " + tc.name);
662 tc.principalOutstanding,
665 tc.nextPaymentDueDate);
667 computedLateInterest == tc.expectedLateInterest,
668 "Late interest mismatch: expected " +
to_string(tc.expectedLateInterest) +
669 ", got " +
to_string(computedLateInterest));
681 Number principalOutstanding;
687 Number expectedAccruedInterest;
692 .name =
"Zero principal outstanding",
693 .principalOutstanding =
Number{0},
694 .periodicRate =
Number{5, -2},
697 .prevPaymentDate = 2'500,
698 .paymentInterval = 30 * 24 * 60 * 60,
699 .expectedAccruedInterest =
Number{0},
702 .name =
"Before start date",
703 .principalOutstanding =
Number{1'000},
704 .periodicRate =
Number{5, -2},
707 .prevPaymentDate = 1'500,
708 .paymentInterval = 30 * 24 * 60 * 60,
709 .expectedAccruedInterest =
Number{0},
712 .name =
"Zero periodic rate",
713 .principalOutstanding =
Number{1'000},
714 .periodicRate =
Number{0},
717 .prevPaymentDate = 2'500,
718 .paymentInterval = 30 * 24 * 60 * 60,
719 .expectedAccruedInterest =
Number{0},
722 .name =
"Zero payment interval",
723 .principalOutstanding =
Number{1'000},
724 .periodicRate =
Number{5, -2},
727 .prevPaymentDate = 2'500,
728 .paymentInterval = 0,
729 .expectedAccruedInterest =
Number{0},
732 .name =
"Standard case",
733 .principalOutstanding =
Number{1'000},
734 .periodicRate =
Number{5, -2},
737 .prevPaymentDate = 2'000,
738 .paymentInterval = 30 * 24 * 60 * 60,
739 .expectedAccruedInterest =
Number{1929012345679012346, -20},
743 for (
auto const& tc : testCases)
745 testcase(
"loanAccruedInterest: " + tc.name);
748 tc.principalOutstanding,
755 computedAccruedInterest == tc.expectedAccruedInterest,
756 "Accrued interest mismatch: expected " +
to_string(tc.expectedAccruedInterest) +
757 ", got " +
to_string(computedAccruedInterest));
772 Number rawPrincipalOutstanding;
779 Number expectedFullPaymentInterest;
784 .name =
"Zero principal outstanding",
785 .rawPrincipalOutstanding =
Number{0},
786 .periodicRate =
Number{5, -2},
788 .paymentInterval = 30 * 24 * 60 * 60,
789 .prevPaymentDate = 2'000,
792 .expectedFullPaymentInterest =
Number{0},
795 .name =
"Zero close interest rate",
796 .rawPrincipalOutstanding =
Number{1'000},
797 .periodicRate =
Number{5, -2},
799 .paymentInterval = 30 * 24 * 60 * 60,
800 .prevPaymentDate = 2'000,
803 .expectedFullPaymentInterest =
Number{1929012345679012346, -20},
806 .name =
"Standard case",
807 .rawPrincipalOutstanding =
Number{1'000},
808 .periodicRate =
Number{5, -2},
810 .paymentInterval = 30 * 24 * 60 * 60,
811 .prevPaymentDate = 2'000,
814 .expectedFullPaymentInterest =
Number{1000192901234567901, -16},
818 for (
auto const& tc : testCases)
820 testcase(
"computeFullPaymentInterest: " + tc.name);
823 tc.rawPrincipalOutstanding,
829 tc.closeInterestRate);
831 computedFullPaymentInterest == tc.expectedFullPaymentInterest,
832 "Full payment interest mismatch: expected " +
833 to_string(tc.expectedFullPaymentInterest) +
", got " +
842 testcase(
"tryOverpayment - No Interest No Fee");
847 Env const env{*
this};
848 Account const issuer{
"issuer"};
853 Number const loanPrincipal{1'000};
856 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
857 Number const overpaymentAmount{50};
882 overpaymentComponents,
883 loanProperties.loanState,
884 loanProperties.periodicPayment,
892 auto const& [actualPaymentParts, newLoanProperties] = *ret;
893 auto const& newState = newLoanProperties.loanState;
897 actualPaymentParts.valueChange == 0,
898 " valueChange mismatch: expected 0, got " +
to_string(actualPaymentParts.valueChange));
901 actualPaymentParts.feePaid == 0,
902 " feePaid mismatch: expected 0, got " +
to_string(actualPaymentParts.feePaid));
905 actualPaymentParts.interestPaid == 0,
906 " interestPaid mismatch: expected 0, got " +
907 to_string(actualPaymentParts.interestPaid));
910 actualPaymentParts.principalPaid == overpaymentAmount,
911 " principalPaid mismatch: expected " +
to_string(overpaymentAmount) +
", got " +
912 to_string(actualPaymentParts.principalPaid));
916 loanProperties.loanState.interestDue - newState.interestDue == 0,
917 " interest change mismatch: expected 0, got " +
918 to_string(loanProperties.loanState.interestDue - newState.interestDue));
921 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
922 " management fee change mismatch: expected 0, got " +
923 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
926 actualPaymentParts.principalPaid ==
927 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
928 " principalPaid mismatch: expected " +
930 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
931 ", got " +
to_string(actualPaymentParts.principalPaid));
937 testcase(
"tryOverpayment - No Interest With Overpayment Fee");
942 Env const env{*
this};
943 Account const issuer{
"issuer"};
948 Number const loanPrincipal{1'000};
951 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
976 overpaymentComponents,
977 loanProperties.loanState,
978 loanProperties.periodicPayment,
986 auto const& [actualPaymentParts, newLoanProperties] = *ret;
987 auto const& newState = newLoanProperties.loanState;
991 actualPaymentParts.valueChange == 0,
992 " valueChange mismatch: expected 0, got " +
to_string(actualPaymentParts.valueChange));
995 actualPaymentParts.feePaid == 5,
996 " feePaid mismatch: expected 5, got " +
to_string(actualPaymentParts.feePaid));
999 actualPaymentParts.principalPaid == 45,
1000 " principalPaid mismatch: expected 45, got `" +
1001 to_string(actualPaymentParts.principalPaid));
1004 actualPaymentParts.interestPaid == 0,
1005 " interestPaid mismatch: expected 0, got " +
1006 to_string(actualPaymentParts.interestPaid));
1011 loanProperties.loanState.interestDue - newState.interestDue == 0,
1012 " interest change mismatch: expected 0, got " +
1013 to_string(loanProperties.loanState.interestDue - newState.interestDue));
1017 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1018 " management fee change mismatch: expected 0, got " +
1019 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1022 actualPaymentParts.principalPaid ==
1023 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1024 " principalPaid mismatch: expected " +
1026 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1027 ", got " +
to_string(actualPaymentParts.principalPaid));
1033 testcase(
"tryOverpayment - Loan Interest, No Overpayment Fees");
1035 using namespace jtx;
1038 Env const env{*
this};
1039 Account const issuer{
"issuer"};
1044 Number const loanPrincipal{1'000};
1047 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1072 overpaymentComponents,
1073 loanProperties.loanState,
1074 loanProperties.periodicPayment,
1082 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1083 auto const& newState = newLoanProperties.loanState;
1089 (actualPaymentParts.valueChange ==
Number{-228802, -5}),
1090 " valueChange mismatch: expected " +
to_string(
Number{-228802, -5}) +
", got " +
1091 to_string(actualPaymentParts.valueChange));
1095 actualPaymentParts.feePaid == 0,
1096 " feePaid mismatch: expected 0, got " +
to_string(actualPaymentParts.feePaid));
1099 actualPaymentParts.principalPaid == 50,
1100 " principalPaid mismatch: expected 50, got `" +
1101 to_string(actualPaymentParts.principalPaid));
1105 actualPaymentParts.interestPaid == 0,
1106 " interestPaid mismatch: expected 0, got " +
1107 to_string(actualPaymentParts.interestPaid));
1111 actualPaymentParts.principalPaid ==
1112 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1113 " principalPaid mismatch: expected " +
1115 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1116 ", got " +
to_string(actualPaymentParts.principalPaid));
1119 actualPaymentParts.valueChange ==
1120 newState.interestDue - loanProperties.loanState.interestDue,
1121 " valueChange mismatch: expected " +
1122 to_string(newState.interestDue - loanProperties.loanState.interestDue) +
", got " +
1123 to_string(actualPaymentParts.valueChange));
1127 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1128 " management fee change mismatch: expected 0, got " +
1129 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1135 testcase(
"tryOverpayment - Loan Interest, Overpayment Interest, No Fee");
1137 using namespace jtx;
1140 Env const env{*
this};
1141 Account const issuer{
"issuer"};
1146 Number const loanPrincipal{1'000};
1149 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1174 overpaymentComponents,
1175 loanProperties.loanState,
1176 loanProperties.periodicPayment,
1184 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1185 auto const& newState = newLoanProperties.loanState;
1190 actualPaymentParts.interestPaid == 5,
1191 " interestPaid mismatch: expected 5, got " +
1192 to_string(actualPaymentParts.interestPaid));
1197 (actualPaymentParts.valueChange ==
1198 Number{-205922, -5} + actualPaymentParts.interestPaid),
1199 " valueChange mismatch: expected " +
1200 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid) +
1201 ", got " +
to_string(actualPaymentParts.valueChange));
1205 actualPaymentParts.feePaid == 0,
1206 " feePaid mismatch: expected 0, got " +
to_string(actualPaymentParts.feePaid));
1209 actualPaymentParts.principalPaid == 45,
1210 " principalPaid mismatch: expected 45, got `" +
1211 to_string(actualPaymentParts.principalPaid));
1215 actualPaymentParts.principalPaid ==
1216 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1217 " principalPaid mismatch: expected " +
1219 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1220 ", got " +
to_string(actualPaymentParts.principalPaid));
1225 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1226 newState.interestDue - loanProperties.loanState.interestDue,
1227 " valueChange mismatch: expected " +
1229 newState.interestDue - loanProperties.loanState.interestDue +
1230 actualPaymentParts.interestPaid) +
1231 ", got " +
to_string(actualPaymentParts.valueChange));
1235 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1236 " management fee change mismatch: expected 0, got " +
1237 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1244 "tryOverpayment - Loan Interest and Fee, Overpayment Interest, No "
1247 using namespace jtx;
1250 Env const env{*
this};
1251 Account const issuer{
"issuer"};
1256 Number const loanPrincipal{1'000};
1259 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1284 overpaymentComponents,
1285 loanProperties.loanState,
1286 loanProperties.periodicPayment,
1294 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1295 auto const& newState = newLoanProperties.loanState;
1302 (actualPaymentParts.interestPaid ==
Number{45, -1}),
1303 " interestPaid mismatch: expected 4.5, got " +
1304 to_string(actualPaymentParts.interestPaid));
1309 (actualPaymentParts.valueChange ==
1310 Number{-18533, -4} + actualPaymentParts.interestPaid),
1311 " valueChange mismatch: expected " +
1312 to_string(
Number{-18533, -4} + actualPaymentParts.interestPaid) +
", got " +
1313 to_string(actualPaymentParts.valueChange));
1318 (actualPaymentParts.feePaid ==
Number{5, -1}),
1319 " feePaid mismatch: expected 0.5, got " +
to_string(actualPaymentParts.feePaid));
1322 actualPaymentParts.principalPaid == 45,
1323 " principalPaid mismatch: expected 45, got `" +
1324 to_string(actualPaymentParts.principalPaid));
1328 actualPaymentParts.principalPaid ==
1329 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1330 " principalPaid mismatch: expected " +
1332 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1333 ", got " +
to_string(actualPaymentParts.principalPaid));
1338 (newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
1340 " management fee change mismatch: expected " +
to_string(
Number{-20592, -5}) +
1342 to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
1345 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1346 newState.interestDue - loanProperties.loanState.interestDue,
1347 " valueChange mismatch: expected " +
1348 to_string(newState.interestDue - loanProperties.loanState.interestDue) +
", got " +
1349 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
1355 testcase(
"tryOverpayment - Loan Interest, Fee, Overpayment Interest, Fee");
1357 using namespace jtx;
1360 Account const issuer{
"issuer"};
1365 Number const loanPrincipal{1'000};
1368 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1370 Env const env{*
this};
1393 Env const env{*
this, features};
1407 overpaymentComponents,
1408 loanProperties.loanState,
1409 loanProperties.periodicPayment,
1414 if (!BEAST_EXPECT(ret))
1415 return std::nullopt;
1417 .parts = ret->first,
1418 .oldState = loanProperties.loanState,
1419 .newState = ret->second.loanState};
1424 if (!fixedOpt || !legacyOpt)
1426 BEAST_EXPECT(fixedOpt.has_value());
1427 BEAST_EXPECT(legacyOpt.has_value());
1430 Outcome
const&
fixed = *fixedOpt;
1431 Outcome
const& legacy = *legacyOpt;
1436 auto checkCommon = [&](Outcome
const& o,
char const* tag) {
1438 (o.parts.interestPaid ==
Number{45, -1}),
1441 (o.parts.feePaid ==
Number{55, -1}),
1444 o.parts.principalPaid == 40,
1447 o.parts.principalPaid ==
1448 o.oldState.principalOutstanding - o.newState.principalOutstanding);
1452 o.parts.valueChange - o.parts.interestPaid ==
1453 o.newState.interestDue - o.oldState.interestDue);
1455 checkCommon(
fixed,
"fixed");
1456 checkCommon(legacy,
"legacy");
1463 BEAST_EXPECT((
fixed.parts.valueChange ==
Number{-164738, -5} +
fixed.parts.interestPaid));
1465 (
fixed.newState.managementFeeDue -
fixed.oldState.managementFeeDue ==
1467 BEAST_EXPECT((legacy.parts.valueChange ==
Number{-164737, -5} + legacy.parts.interestPaid));
1469 (legacy.newState.managementFeeDue - legacy.oldState.managementFeeDue ==
1477 using namespace jtx;
1479 Account const issuer{
"issuer"};
1496 .name =
"Zero amount",
1497 .coverAvailable =
Number{10},
1502 .name =
"Rounds to zero at cover scale",
1503 .coverAvailable =
Number{10},
1508 .name =
"Zero coverAvailable, whole-unit amount",
1511 .coverAvailable =
Number{0},
1516 .name =
"Supra-ULP amount",
1517 .coverAvailable =
Number{10},
1523 Env const env{*
this};
1525 for (
auto const& tc : testCases)
1527 testcase(
"canApplyToBrokerCover: " + tc.name);
1529 sle->at(sfCoverAvailable) = tc.coverAvailable;
1537 testcase(
"canApplyToBrokerCover: amendment disabled");
1540 sle->at(sfCoverAvailable) =
Number{10};
TestcaseT testcase
Memberspace for declaring test cases.
std::chrono::time_point< NetClock > time_point
std::chrono::duration< rep, period > duration
Number is a floating point type that can represent a wide range of values.
void testComputePaymentFactor()
void testComputeTheoreticalLoanStateNearZeroRate()
void testLoanPrincipalFromPeriodicPayment()
void testTryOverpaymentLoanInterestNoOverpaymentFees()
void testComputePowerMinusOne()
void testComputeInterestAndFeeParts()
void testTryOverpaymentNoInterestOverpaymentFee()
void testTryOverpaymentNoInterestNoFee()
void testTryOverpaymentLoanInterestFeeOverpaymentInterestNoFee()
void testTryOverpaymentLoanInterestFeeOverpaymentInterestFee()
void testComputeFullPaymentInterest()
void testLoanPeriodicPayment()
void testCanApplyToBrokerCover()
void testComputeOverpaymentComponents()
void testLoanPrincipalFromPeriodicPaymentNearZeroRate()
void testTryOverpaymentLoanInterestOverpaymentInterest()
void testComputePaymentFactorNearZeroRate()
void run() override
Runs the suite.
void testComputePowerMinusOneHybrid()
void testLoanAccruedInterest()
void testLoanLatePaymentInterest()
Immutable cryptographic account descriptor.
A transaction testing environment.
beast::Journal const journal
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
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()
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
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).
Number power(Number const &f, unsigned n)
TenthBips< std::uint32_t > TenthBips32
static FunctionType fixed(Keylet const &keylet)
TenthBips< std::uint16_t > TenthBips16
std::string to_string(BaseUInt< Bits, Tag > const &a)
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
TERSubset< CanCvtToTER > TER
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)
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.