1#include <xrpl/beast/unit_test/suite.h>
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/fee.h>
8#include <test/jtx/pay.h>
9#include <test/jtx/sig.h>
10#include <test/jtx/vault.h>
12#include <xrpl/basics/Number.h>
13#include <xrpl/basics/chrono.h>
14#include <xrpl/ledger/helpers/LendingHelpers.h>
15#include <xrpl/protocol/Feature.h>
16#include <xrpl/protocol/Indexes.h>
17#include <xrpl/protocol/Issue.h>
18#include <xrpl/protocol/LedgerFormats.h>
19#include <xrpl/protocol/Protocol.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/STAmount.h>
22#include <xrpl/protocol/STLedgerEntry.h>
23#include <xrpl/protocol/SeqProxy.h>
24#include <xrpl/protocol/TER.h>
25#include <xrpl/protocol/TxFlags.h>
26#include <xrpl/protocol/Units.h>
45 auto const& rules = env.
current()->rules();
51 Number expectedPaymentFactor;
56 .name =
"Zero periodic rate",
58 .paymentsRemaining = 4,
59 .expectedPaymentFactor =
Number{25, -2},
62 .name =
"One payment remaining",
63 .periodicRate =
Number{5, -2},
64 .paymentsRemaining = 1,
65 .expectedPaymentFactor =
Number{105, -2},
68 .name =
"Multiple payments remaining",
69 .periodicRate =
Number{5, -2},
70 .paymentsRemaining = 3,
71 .expectedPaymentFactor =
Number{3672085646312450436, -19},
74 .name =
"Zero payments remaining",
75 .periodicRate =
Number{5, -2},
76 .paymentsRemaining = 0,
77 .expectedPaymentFactor =
Number{0},
81 for (
auto const& tc : testCases)
83 testcase(
"computePaymentFactor: " + tc.name);
85 auto const computedPaymentFactor =
88 computedPaymentFactor == tc.expectedPaymentFactor,
89 "Payment factor mismatch: expected " +
to_string(tc.expectedPaymentFactor) +
90 ", got " +
to_string(computedPaymentFactor));
100 auto const& rules = env.
current()->rules();
105 Number principalOutstanding;
108 Number expectedPeriodicPayment;
113 .name =
"Zero principal outstanding",
114 .principalOutstanding =
Number{0},
115 .periodicRate =
Number{5, -2},
116 .paymentsRemaining = 5,
117 .expectedPeriodicPayment =
Number{0},
120 .name =
"Zero payments remaining",
121 .principalOutstanding =
Number{1'000},
122 .periodicRate =
Number{5, -2},
123 .paymentsRemaining = 0,
124 .expectedPeriodicPayment =
Number{0},
127 .name =
"Zero periodic rate",
128 .principalOutstanding =
Number{1'000},
129 .periodicRate =
Number{0},
130 .paymentsRemaining = 4,
131 .expectedPeriodicPayment =
Number{250},
134 .name =
"Standard case",
135 .principalOutstanding =
Number{1'000},
137 .paymentsRemaining = 3,
138 .expectedPeriodicPayment =
Number{389569066396123265, -15},
142 for (
auto const& tc : testCases)
144 testcase(
"loanPeriodicPayment: " + tc.name);
147 rules, tc.principalOutstanding, tc.periodicRate, tc.paymentsRemaining);
149 computedPeriodicPayment == tc.expectedPeriodicPayment,
150 "Periodic payment mismatch: expected " +
to_string(tc.expectedPeriodicPayment) +
151 ", got " +
to_string(computedPeriodicPayment));
160 Env const env{*
this};
161 auto const& rules = env.
current()->rules();
169 Number expectedPrincipalOutstanding;
174 .name =
"Zero periodic payment",
175 .periodicPayment =
Number{0},
176 .periodicRate =
Number{5, -2},
177 .paymentsRemaining = 5,
178 .expectedPrincipalOutstanding =
Number{0},
181 .name =
"Zero payments remaining",
182 .periodicPayment =
Number{1'000},
183 .periodicRate =
Number{5, -2},
184 .paymentsRemaining = 0,
185 .expectedPrincipalOutstanding =
Number{0},
188 .name =
"Zero periodic rate",
189 .periodicPayment =
Number{250},
190 .periodicRate =
Number{0},
191 .paymentsRemaining = 4,
192 .expectedPrincipalOutstanding =
Number{1'000},
195 .name =
"Standard case",
196 .periodicPayment =
Number{389569066396123265, -15},
198 .paymentsRemaining = 3,
199 .expectedPrincipalOutstanding =
Number{1'000},
203 for (
auto const& tc : testCases)
205 testcase(
"loanPrincipalFromPeriodicPayment: " + tc.name);
208 rules, tc.periodicPayment, tc.periodicRate, tc.paymentsRemaining);
210 computedPrincipalOutstanding == tc.expectedPrincipalOutstanding,
211 "Principal outstanding mismatch: expected " +
212 to_string(tc.expectedPrincipalOutstanding) +
", got " +
213 to_string(computedPrincipalOutstanding));
225 testcase(
"computePowerMinusOne: zero rate returns zero");
229 testcase(
"computePowerMinusOne: zero paymentsRemaining returns zero");
230 Number const fivePercent{5, -2};
235 testcase(
"computePowerMinusOne: standard case (1.05)^3 - 1 = 0.157625");
237 Number const expected{157625, -6};
242 testcase(
"computePowerMinusOne: r=1, n=1");
252 testcase(
"computePowerMinusOne: near-zero rate matches independent 2r + r^2");
256 Number const independentExpected = 2 * r + r * r;
261 testcase(
"computePowerMinusOne: near-zero rate matches independent 3r + 3r^2 + r^3");
263 Number const independentExpected = 3 * r + 3 * r * r + r * r * r;
274 testcase(
"computePowerMinusOne: large n, early termination matches hybrid output");
299 testcase(
"computePowerMinusOneHybrid: r*n >= 1e-9 uses closed form (bit-exact match)");
301 struct AboveThreshold
308 {.name =
"r=5%, n=3", .r =
Number{5, -2}, .n = 3},
309 {.name =
"r=0.1%, n=1000", .r =
Number{1, -3}, .n = 1'000},
310 {.name =
"r=1e-7, n=100 (above threshold by 10x)", .r =
Number{1, -7}, .n = 100},
312 for (
auto const& tc : cases)
328 "computePowerMinusOneHybrid: r*n < 1e-9 uses binomial expansion (bit-exact match)");
330 struct BelowThreshold
339 {.name =
"bug regime: r~1.9e-10, n=2",
342 {.name =
"r=1e-12, n=100", .r =
Number{1, -12}, .n = 100},
344 for (
auto const& tc : cases)
356 testcase(
"computePowerMinusOneHybrid: edge cases");
357 Number const fivePercent{5, -2};
369 testcase(
"computePowerMinusOneHybrid: threshold boundary r*n = 1e-9");
379 {.name =
"r=1e-9, n=1", .r =
Number{1, -9}, .n = 1},
380 {.name =
"r=1e-12, n=1000", .r =
Number{1, -12}, .n = 1'000},
383 for (
auto const& tc : cases)
393 tc.name +
": hybrid should equal closed at threshold; got hybrid=" +
399 Number const tolerance{1, -18};
403 tc.name +
": closed and binomial diverge at threshold by " +
to_string(diff));
415 testcase(
"loanPrincipalFromPeriodicPayment: principal <= payment*n at near-zero rate");
418 Env const env{*
this};
419 auto const& rules = env.
current()->rules();
428 for (
auto const n : {3u, 2u, 1u})
430 auto const computed =
432 auto const upperBound = periodicPayment *
Number{n};
434 computed <= upperBound,
447 testcase(
"computeTheoreticalLoanState: non-negative interestDue at near-zero rate");
450 Env const env{*
this};
451 auto const& rules = env.
current()->rules();
459 BEAST_EXPECT(state.principalOutstanding <= state.valueOutstanding);
460 BEAST_EXPECT(state.interestDue >= 0);
461 BEAST_EXPECT(state.managementFeeDue == 0);
472 testcase(
"computePaymentFactor: near-zero rate, amendment disabled vs enabled");
484 Number const reference = (1 + 3 * r + 3 * r * r + r * r * r) / (3 + 3 * r + r * r);
492 Env const envFix{*
this};
496 BEAST_EXPECT(buggyFactor != correctFactor);
500 BEAST_EXPECT(
abs(correctFactor - reference) <
Number(1, -15));
504 BEAST_EXPECT(
abs(buggyFactor - reference) >
Number(1, -12));
510 testcase(
"computeOverpaymentComponents");
514 Account const issuer{
"issuer"};
516 int32_t
const loanScale = 1;
517 auto const overpayment =
Number{1'000};
518 auto const overpaymentInterestRate =
TenthBips32{10'000};
519 auto const overpaymentFeeRate =
TenthBips32{50'000};
520 auto const managementFeeRate =
TenthBips16{10'000};
522 auto const expectedOverpaymentFee =
Number{500};
523 auto const expectedOverpaymentInterestGross =
Number{100};
524 auto const expectedOverpaymentInterestNet =
Number{90};
525 auto const expectedOverpaymentManagementFee =
Number{10};
526 auto const expectedPrincipalPortion =
Number{400};
528 Env const env{*
this};
534 overpaymentInterestRate,
538 BEAST_EXPECT(components.untrackedManagementFee == expectedOverpaymentFee);
540 BEAST_EXPECT(components.untrackedInterest == expectedOverpaymentInterestNet);
542 BEAST_EXPECT(components.trackedInterestPart() == expectedOverpaymentInterestNet);
544 BEAST_EXPECT(components.trackedManagementFeeDelta == expectedOverpaymentManagementFee);
545 BEAST_EXPECT(components.trackedPrincipalDelta == expectedPrincipalPortion);
547 components.trackedManagementFeeDelta + components.untrackedInterest ==
548 expectedOverpaymentInterestGross);
551 components.trackedManagementFeeDelta + components.untrackedInterest +
552 components.trackedPrincipalDelta + components.untrackedManagementFee ==
567 Number expectedInterestPart;
571 Account const issuer{
"issuer"};
576 {.name =
"Zero interest",
579 .expectedInterestPart =
Number{0},
580 .expectedFeePart =
Number{0}},
581 {.name =
"Zero fee rate",
582 .interest =
Number{1'000},
584 .expectedInterestPart =
Number{1'000},
585 .expectedFeePart =
Number{0}},
586 {.name =
"10% fee rate",
587 .interest =
Number{1'000},
589 .expectedInterestPart =
Number{900},
590 .expectedFeePart =
Number{100}},
593 for (
auto const& tc : testCases)
595 testcase(
"computeInterestAndFeeParts: " + tc.name);
597 auto const [computedInterestPart, computedFeePart] =
600 computedInterestPart == tc.expectedInterestPart,
601 "Interest part mismatch: expected " +
to_string(tc.expectedInterestPart) +
602 ", got " +
to_string(computedInterestPart));
604 computedFeePart == tc.expectedFeePart,
605 "Fee part mismatch: expected " +
to_string(tc.expectedFeePart) +
", got " +
618 Number principalOutstanding;
622 Number expectedLateInterest;
627 .name =
"On-time payment",
628 .principalOutstanding =
Number{1'000},
631 .nextPaymentDueDate = 3'000,
632 .expectedLateInterest =
Number{0},
635 .name =
"Early payment",
636 .principalOutstanding =
Number{1'000},
639 .nextPaymentDueDate = 4'000,
640 .expectedLateInterest =
Number{0},
643 .name =
"No principal outstanding",
644 .principalOutstanding =
Number{0},
647 .nextPaymentDueDate = 2'000,
648 .expectedLateInterest =
Number{0},
651 .name =
"No late interest rate",
652 .principalOutstanding =
Number{1'000},
655 .nextPaymentDueDate = 2'000,
656 .expectedLateInterest =
Number{0},
659 .name =
"Late payment",
660 .principalOutstanding =
Number{1'000},
663 .nextPaymentDueDate = 2'000,
664 .expectedLateInterest =
Number{317097919837645865, -19},
668 for (
auto const& tc : testCases)
670 testcase(
"loanLatePaymentInterest: " + tc.name);
673 tc.principalOutstanding,
676 tc.nextPaymentDueDate);
678 computedLateInterest == tc.expectedLateInterest,
679 "Late interest mismatch: expected " +
to_string(tc.expectedLateInterest) +
680 ", got " +
to_string(computedLateInterest));
692 Number principalOutstanding;
698 Number expectedAccruedInterest;
703 .name =
"Zero principal outstanding",
704 .principalOutstanding =
Number{0},
705 .periodicRate =
Number{5, -2},
708 .prevPaymentDate = 2'500,
709 .paymentInterval = 30 * 24 * 60 * 60,
710 .expectedAccruedInterest =
Number{0},
713 .name =
"Before start date",
714 .principalOutstanding =
Number{1'000},
715 .periodicRate =
Number{5, -2},
718 .prevPaymentDate = 1'500,
719 .paymentInterval = 30 * 24 * 60 * 60,
720 .expectedAccruedInterest =
Number{0},
723 .name =
"Zero periodic rate",
724 .principalOutstanding =
Number{1'000},
725 .periodicRate =
Number{0},
728 .prevPaymentDate = 2'500,
729 .paymentInterval = 30 * 24 * 60 * 60,
730 .expectedAccruedInterest =
Number{0},
733 .name =
"Zero payment interval",
734 .principalOutstanding =
Number{1'000},
735 .periodicRate =
Number{5, -2},
738 .prevPaymentDate = 2'500,
739 .paymentInterval = 0,
740 .expectedAccruedInterest =
Number{0},
743 .name =
"Standard case",
744 .principalOutstanding =
Number{1'000},
745 .periodicRate =
Number{5, -2},
748 .prevPaymentDate = 2'000,
749 .paymentInterval = 30 * 24 * 60 * 60,
750 .expectedAccruedInterest =
Number{1929012345679012346, -20},
754 for (
auto const& tc : testCases)
756 testcase(
"loanAccruedInterest: " + tc.name);
759 tc.principalOutstanding,
766 computedAccruedInterest == tc.expectedAccruedInterest,
767 "Accrued interest mismatch: expected " +
to_string(tc.expectedAccruedInterest) +
768 ", got " +
to_string(computedAccruedInterest));
783 Number rawPrincipalOutstanding;
790 Number expectedFullPaymentInterest;
795 .name =
"Zero principal outstanding",
796 .rawPrincipalOutstanding =
Number{0},
797 .periodicRate =
Number{5, -2},
799 .paymentInterval = 30 * 24 * 60 * 60,
800 .prevPaymentDate = 2'000,
803 .expectedFullPaymentInterest =
Number{0},
806 .name =
"Zero close interest rate",
807 .rawPrincipalOutstanding =
Number{1'000},
808 .periodicRate =
Number{5, -2},
810 .paymentInterval = 30 * 24 * 60 * 60,
811 .prevPaymentDate = 2'000,
814 .expectedFullPaymentInterest =
Number{1929012345679012346, -20},
817 .name =
"Standard case",
818 .rawPrincipalOutstanding =
Number{1'000},
819 .periodicRate =
Number{5, -2},
821 .paymentInterval = 30 * 24 * 60 * 60,
822 .prevPaymentDate = 2'000,
825 .expectedFullPaymentInterest =
Number{1000192901234567901, -16},
829 for (
auto const& tc : testCases)
831 testcase(
"computeFullPaymentInterest: " + tc.name);
834 tc.rawPrincipalOutstanding,
840 tc.closeInterestRate);
842 computedFullPaymentInterest == tc.expectedFullPaymentInterest,
843 "Full payment interest mismatch: expected " +
844 to_string(tc.expectedFullPaymentInterest) +
", got " +
853 testcase(
"tryOverpayment - No Interest No Fee");
858 Env const env{*
this};
859 Account const issuer{
"issuer"};
864 Number const loanPrincipal{1'000};
867 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
868 Number const overpaymentAmount{50};
893 overpaymentComponents,
894 loanProperties.loanState,
895 loanProperties.periodicPayment,
903 auto const& [actualPaymentParts, newLoanProperties] = *ret;
904 auto const& newState = newLoanProperties.loanState;
908 actualPaymentParts.valueChange == 0,
909 " valueChange mismatch: expected 0, got " +
to_string(actualPaymentParts.valueChange));
912 actualPaymentParts.feePaid == 0,
913 " feePaid mismatch: expected 0, got " +
to_string(actualPaymentParts.feePaid));
916 actualPaymentParts.interestPaid == 0,
917 " interestPaid mismatch: expected 0, got " +
918 to_string(actualPaymentParts.interestPaid));
921 actualPaymentParts.principalPaid == overpaymentAmount,
922 " principalPaid mismatch: expected " +
to_string(overpaymentAmount) +
", got " +
923 to_string(actualPaymentParts.principalPaid));
927 loanProperties.loanState.interestDue - newState.interestDue == 0,
928 " interest change mismatch: expected 0, got " +
929 to_string(loanProperties.loanState.interestDue - newState.interestDue));
932 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
933 " management fee change mismatch: expected 0, got " +
934 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
937 actualPaymentParts.principalPaid ==
938 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
939 " principalPaid mismatch: expected " +
941 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
942 ", got " +
to_string(actualPaymentParts.principalPaid));
948 testcase(
"tryOverpayment - No Interest With Overpayment Fee");
953 Env const env{*
this};
954 Account const issuer{
"issuer"};
959 Number const loanPrincipal{1'000};
962 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
987 overpaymentComponents,
988 loanProperties.loanState,
989 loanProperties.periodicPayment,
997 auto const& [actualPaymentParts, newLoanProperties] = *ret;
998 auto const& newState = newLoanProperties.loanState;
1002 actualPaymentParts.valueChange == 0,
1003 " valueChange mismatch: expected 0, got " +
to_string(actualPaymentParts.valueChange));
1006 actualPaymentParts.feePaid == 5,
1007 " feePaid mismatch: expected 5, got " +
to_string(actualPaymentParts.feePaid));
1010 actualPaymentParts.principalPaid == 45,
1011 " principalPaid mismatch: expected 45, got `" +
1012 to_string(actualPaymentParts.principalPaid));
1015 actualPaymentParts.interestPaid == 0,
1016 " interestPaid mismatch: expected 0, got " +
1017 to_string(actualPaymentParts.interestPaid));
1022 loanProperties.loanState.interestDue - newState.interestDue == 0,
1023 " interest change mismatch: expected 0, got " +
1024 to_string(loanProperties.loanState.interestDue - newState.interestDue));
1028 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1029 " management fee change mismatch: expected 0, got " +
1030 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1033 actualPaymentParts.principalPaid ==
1034 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1035 " principalPaid mismatch: expected " +
1037 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1038 ", got " +
to_string(actualPaymentParts.principalPaid));
1044 testcase(
"tryOverpayment - Loan Interest, No Overpayment Fees");
1046 using namespace jtx;
1049 Env const env{*
this};
1050 Account const issuer{
"issuer"};
1055 Number const loanPrincipal{1'000};
1058 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1083 overpaymentComponents,
1084 loanProperties.loanState,
1085 loanProperties.periodicPayment,
1093 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1094 auto const& newState = newLoanProperties.loanState;
1100 (actualPaymentParts.valueChange ==
Number{-228802, -5}),
1101 " valueChange mismatch: expected " +
to_string(
Number{-228802, -5}) +
", got " +
1102 to_string(actualPaymentParts.valueChange));
1106 actualPaymentParts.feePaid == 0,
1107 " feePaid mismatch: expected 0, got " +
to_string(actualPaymentParts.feePaid));
1110 actualPaymentParts.principalPaid == 50,
1111 " principalPaid mismatch: expected 50, got `" +
1112 to_string(actualPaymentParts.principalPaid));
1116 actualPaymentParts.interestPaid == 0,
1117 " interestPaid mismatch: expected 0, got " +
1118 to_string(actualPaymentParts.interestPaid));
1122 actualPaymentParts.principalPaid ==
1123 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1124 " principalPaid mismatch: expected " +
1126 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1127 ", got " +
to_string(actualPaymentParts.principalPaid));
1130 actualPaymentParts.valueChange ==
1131 newState.interestDue - loanProperties.loanState.interestDue,
1132 " valueChange mismatch: expected " +
1133 to_string(newState.interestDue - loanProperties.loanState.interestDue) +
", got " +
1134 to_string(actualPaymentParts.valueChange));
1138 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1139 " management fee change mismatch: expected 0, got " +
1140 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1146 testcase(
"tryOverpayment - Loan Interest, Overpayment Interest, No Fee");
1148 using namespace jtx;
1151 Env const env{*
this};
1152 Account const issuer{
"issuer"};
1157 Number const loanPrincipal{1'000};
1160 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1185 overpaymentComponents,
1186 loanProperties.loanState,
1187 loanProperties.periodicPayment,
1195 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1196 auto const& newState = newLoanProperties.loanState;
1201 actualPaymentParts.interestPaid == 5,
1202 " interestPaid mismatch: expected 5, got " +
1203 to_string(actualPaymentParts.interestPaid));
1208 (actualPaymentParts.valueChange ==
1209 Number{-205922, -5} + actualPaymentParts.interestPaid),
1210 " valueChange mismatch: expected " +
1211 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid) +
1212 ", got " +
to_string(actualPaymentParts.valueChange));
1216 actualPaymentParts.feePaid == 0,
1217 " feePaid mismatch: expected 0, got " +
to_string(actualPaymentParts.feePaid));
1220 actualPaymentParts.principalPaid == 45,
1221 " principalPaid mismatch: expected 45, got `" +
1222 to_string(actualPaymentParts.principalPaid));
1226 actualPaymentParts.principalPaid ==
1227 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1228 " principalPaid mismatch: expected " +
1230 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1231 ", got " +
to_string(actualPaymentParts.principalPaid));
1236 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1237 newState.interestDue - loanProperties.loanState.interestDue,
1238 " valueChange mismatch: expected " +
1240 newState.interestDue - loanProperties.loanState.interestDue +
1241 actualPaymentParts.interestPaid) +
1242 ", got " +
to_string(actualPaymentParts.valueChange));
1246 loanProperties.loanState.managementFeeDue - newState.managementFeeDue == 0,
1247 " management fee change mismatch: expected 0, got " +
1248 to_string(loanProperties.loanState.managementFeeDue - newState.managementFeeDue));
1255 "tryOverpayment - Loan Interest and Fee, Overpayment Interest, No "
1258 using namespace jtx;
1261 Env const env{*
this};
1262 Account const issuer{
"issuer"};
1267 Number const loanPrincipal{1'000};
1270 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1295 overpaymentComponents,
1296 loanProperties.loanState,
1297 loanProperties.periodicPayment,
1305 auto const& [actualPaymentParts, newLoanProperties] = *ret;
1306 auto const& newState = newLoanProperties.loanState;
1313 (actualPaymentParts.interestPaid ==
Number{45, -1}),
1314 " interestPaid mismatch: expected 4.5, got " +
1315 to_string(actualPaymentParts.interestPaid));
1320 (actualPaymentParts.valueChange ==
1321 Number{-18533, -4} + actualPaymentParts.interestPaid),
1322 " valueChange mismatch: expected " +
1323 to_string(
Number{-18533, -4} + actualPaymentParts.interestPaid) +
", got " +
1324 to_string(actualPaymentParts.valueChange));
1329 (actualPaymentParts.feePaid ==
Number{5, -1}),
1330 " feePaid mismatch: expected 0.5, got " +
to_string(actualPaymentParts.feePaid));
1333 actualPaymentParts.principalPaid == 45,
1334 " principalPaid mismatch: expected 45, got `" +
1335 to_string(actualPaymentParts.principalPaid));
1339 actualPaymentParts.principalPaid ==
1340 loanProperties.loanState.principalOutstanding - newState.principalOutstanding,
1341 " principalPaid mismatch: expected " +
1343 loanProperties.loanState.principalOutstanding - newState.principalOutstanding) +
1344 ", got " +
to_string(actualPaymentParts.principalPaid));
1349 (newState.managementFeeDue - loanProperties.loanState.managementFeeDue ==
1351 " management fee change mismatch: expected " +
to_string(
Number{-20592, -5}) +
1353 to_string(newState.managementFeeDue - loanProperties.loanState.managementFeeDue));
1356 actualPaymentParts.valueChange - actualPaymentParts.interestPaid ==
1357 newState.interestDue - loanProperties.loanState.interestDue,
1358 " valueChange mismatch: expected " +
1359 to_string(newState.interestDue - loanProperties.loanState.interestDue) +
", got " +
1360 to_string(actualPaymentParts.valueChange - actualPaymentParts.interestPaid));
1366 testcase(
"tryOverpayment - Loan Interest, Fee, Overpayment Interest, Fee");
1368 using namespace jtx;
1371 Account const issuer{
"issuer"};
1376 Number const loanPrincipal{1'000};
1379 auto const periodicRate =
loanPeriodicRate(loanInterestRate, paymentInterval);
1381 Env const env{*
this};
1404 Env const env{*
this, features};
1418 overpaymentComponents,
1419 loanProperties.loanState,
1420 loanProperties.periodicPayment,
1425 if (!BEAST_EXPECT(ret))
1426 return std::nullopt;
1428 .parts = ret->first,
1429 .oldState = loanProperties.loanState,
1430 .newState = ret->second.loanState};
1435 if (!fixedOpt || !legacyOpt)
1437 BEAST_EXPECT(fixedOpt.has_value());
1438 BEAST_EXPECT(legacyOpt.has_value());
1441 Outcome
const&
fixed = *fixedOpt;
1442 Outcome
const& legacy = *legacyOpt;
1447 auto checkCommon = [&](Outcome
const& o,
char const* tag) {
1449 (o.parts.interestPaid ==
Number{45, -1}),
1452 (o.parts.feePaid ==
Number{55, -1}),
1455 o.parts.principalPaid == 40,
1458 o.parts.principalPaid ==
1459 o.oldState.principalOutstanding - o.newState.principalOutstanding);
1463 o.parts.valueChange - o.parts.interestPaid ==
1464 o.newState.interestDue - o.oldState.interestDue);
1466 checkCommon(
fixed,
"fixed");
1467 checkCommon(legacy,
"legacy");
1474 BEAST_EXPECT((
fixed.parts.valueChange ==
Number{-164738, -5} +
fixed.parts.interestPaid));
1476 (
fixed.newState.managementFeeDue -
fixed.oldState.managementFeeDue ==
1478 BEAST_EXPECT((legacy.parts.valueChange ==
Number{-164737, -5} + legacy.parts.interestPaid));
1480 (legacy.newState.managementFeeDue - legacy.oldState.managementFeeDue ==
1492 Number principalRequested;
1497 {.name =
"Zero interest",
1498 .principalRequested =
Number{1'000},
1499 .interestDue =
Number{0}},
1500 {.name =
"Nonzero interest",
1501 .principalRequested =
Number{1'000},
1502 .interestDue =
Number{75}},
1505 for (
auto const& tc : testCases)
1507 testcase(
"instant_recognition::loanOriginationDeltas: " + tc.name);
1511 deltas.assetsTotalDelta == tc.interestDue,
1512 "assetsTotalDelta mismatch: expected " +
to_string(tc.interestDue) +
", got " +
1515 deltas.debtTotalDelta == tc.principalRequested + tc.interestDue,
1516 "debtTotalDelta mismatch: expected " +
1517 to_string(tc.principalRequested + tc.interestDue) +
", got " +
1527 testcase(
"cash_basis::loanOriginationDeltas: interestDue is ignored");
1529 Number const principalRequested{1'000};
1530 Number const interestDue{75};
1534 deltas.assetsTotalDelta == 0,
1535 "assetsTotalDelta mismatch: expected 0, got " +
to_string(deltas.assetsTotalDelta));
1537 deltas.debtTotalDelta == principalRequested,
1538 "debtTotalDelta mismatch: expected " +
to_string(principalRequested) +
", got " +
1557 {.name =
"No maximum configured",
1558 .vaultMaximum =
Number{0},
1559 .vaultTotal =
Number{900},
1560 .interestDue =
Number{1'000},
1562 {.name =
"Interest fits under headroom",
1563 .vaultMaximum =
Number{1'000},
1564 .vaultTotal =
Number{900},
1565 .interestDue =
Number{50},
1567 {.name =
"Interest exactly fills headroom",
1568 .vaultMaximum =
Number{1'000},
1569 .vaultTotal =
Number{900},
1570 .interestDue =
Number{100},
1572 {.name =
"Interest exceeds headroom",
1573 .vaultMaximum =
Number{1'000},
1574 .vaultTotal =
Number{900},
1575 .interestDue =
Number{101},
1579 for (
auto const& tc : testCases)
1581 testcase(
"instant_recognition::loanOriginationExceedsVaultMaximum: " + tc.name);
1584 tc.vaultMaximum, tc.vaultTotal, tc.interestDue) == tc.expected);
1593 Number const& totalValueOutstanding,
1594 Number const& principalOutstanding,
1595 Number const& managementFeeOutstanding)
1598 sle->at(sfTotalValueOutstanding) = totalValueOutstanding;
1599 sle->at(sfPrincipalOutstanding) = principalOutstanding;
1600 sle->at(sfManagementFeeOutstanding) = managementFeeOutstanding;
1614 sle->at(sfLEVersion) = std::to_underlying(*leVersion);
1616 sle->at(sfAssetsMaximum) = *assetsMaximum;
1618 sle->at(sfAssetsTotal) = *assetsTotal;
1625 testcase(
"instant_recognition::loanVaultExposure");
1634 testcase(
"cash_basis::loanVaultExposure");
1647 .principalPaid =
Number{100},
1648 .interestPaid =
Number{20},
1649 .valueChange =
Number{5},
1653 testcase(
"instant_recognition::loanPaymentDeltas: nonzero valueChange");
1655 BEAST_EXPECT(deltas.assetsTotalDelta == parts.
valueChange);
1657 deltas.debtTotalDelta ==
1662 testcase(
"cash_basis::loanPaymentDeltas: nonzero valueChange ignored");
1664 BEAST_EXPECT(deltas.assetsTotalDelta == parts.
interestPaid);
1672 using namespace jtx;
1674 Number const principalRequested{1'000};
1675 Number const interestDue{75};
1682 "loanOriginationDeltas dispatcher: amendment enabled, legacy vault picks "
1683 "InstantRecognition");
1684 Env const env{*
this};
1686 auto const expected =
1688 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1689 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1694 "loanOriginationDeltas dispatcher: amendment enabled, LEVersion == "
1695 "VaultVersion::CashBasis picks CashBasis");
1696 Env const env{*
this};
1700 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1701 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1708 using namespace jtx;
1710 Number const vaultMaximum{1'000};
1711 Number const vaultTotal{900};
1713 Number const interestDue{101};
1715 auto const legacyVault =
makeVaultSle(std::nullopt, vaultMaximum, vaultTotal);
1720 "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, legacy vault "
1721 "picks InstantRecognition");
1722 Env const env{*
this};
1726 vaultMaximum, vaultTotal, interestDue));
1731 "loanOriginationExceedsVaultMaximum dispatcher: amendment enabled, LEVersion == "
1732 "VaultVersion::CashBasis picks CashBasis");
1733 Env const env{*
this};
1743 using namespace jtx;
1750 "loanVaultExposure dispatcher: amendment enabled, legacy vault picks "
1751 "InstantRecognition");
1752 Env const env{*
this};
1761 "loanVaultExposure dispatcher: amendment enabled, LEVersion == "
1762 "VaultVersion::CashBasis "
1764 Env const env{*
this};
1774 using namespace jtx;
1777 .principalPaid =
Number{100},
1778 .interestPaid =
Number{20},
1779 .valueChange =
Number{5},
1787 "loanPaymentDeltas dispatcher: amendment enabled, legacy vault picks "
1788 "InstantRecognition");
1789 Env const env{*
this};
1792 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1793 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1798 "loanPaymentDeltas dispatcher: amendment enabled, LEVersion == "
1799 "VaultVersion::CashBasis "
1801 Env const env{*
this};
1804 BEAST_EXPECT(deltas.assetsTotalDelta == expected.assetsTotalDelta);
1805 BEAST_EXPECT(deltas.debtTotalDelta == expected.debtTotalDelta);
1813 using namespace jtx;
1815 Account const issuer{
"issuer"};
1832 .name =
"Zero amount",
1833 .coverAvailable =
Number{10},
1838 .name =
"Rounds to zero at cover scale",
1839 .coverAvailable =
Number{10},
1844 .name =
"Zero coverAvailable, whole-unit amount",
1847 .coverAvailable =
Number{0},
1852 .name =
"Supra-ULP amount",
1853 .coverAvailable =
Number{10},
1859 Env const env{*
this};
1861 for (
auto const& tc : testCases)
1863 testcase(
"canApplyToBrokerCover: " + tc.name);
1865 sle->at(sfCoverAvailable) = tc.coverAvailable;
1873 testcase(
"canApplyToBrokerCover: amendment disabled");
1876 sle->at(sfCoverAvailable) =
Number{10};
1896 using namespace jtx;
1897 using namespace loan;
1899 testcase(
"getLoanDefaultFreezeExemptAccounts");
1901 Account const lender{
"lender"};
1902 Account const borrower{
"borrower"};
1905 Vault const vault{env};
1906 env.
fund(
XRP(10'000), lender, borrower);
1913 auto [vaultTx, vaultKeylet, subscriptionDate] =
1914 vault.createClosedEnded({.owner = lender, .asset =
xrpIssue()});
1917 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = XRP(1'000)}));
1920 vault.closePastSubscription(subscriptionDate);
1922 auto const brokerKeylet =
1927 env(
set(borrower, brokerKeylet.key,
Number{200'000}),
1928 Sig(sfCounterpartySignature, lender),
1942 auto const jt = env.
jt(manage(lender, loanKeylet.key, tfLoanImpair));
1949 auto const jt = env.
jt(manage(lender, loanKeylet.key, tfLoanDefault));
1958 auto const jt = env.
jt(manage(lender, brokerKeylet.key, tfLoanDefault));
1965 auto const jt = env.
jt(manage(lender, loanKeylet.key, tfLoanDefault));
1967 auto const brokerSle = env.
le(brokerKeylet);
1968 auto const vaultSle = env.
le(vaultKeylet);
1969 BEAST_EXPECT(result);
1970 BEAST_EXPECT(brokerSle);
1971 BEAST_EXPECT(vaultSle);
1972 if (result && brokerSle && vaultSle)
1974 BEAST_EXPECT(result->issuer == vaultSle->at(sfAsset).getIssuer());
1975 BEAST_EXPECT(result->broker == brokerSle->at(sfAccount));
1976 BEAST_EXPECT(result->vault == vaultSle->at(sfAccount));
1977 BEAST_EXPECT(result->asset == vaultSle->at(sfAsset));
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.
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
void testComputePaymentFactor()
void testLoanOriginationExceedsVaultMaximumDispatcher()
void testInstantRecognitionLoanOriginationDeltas()
void testComputeTheoreticalLoanStateNearZeroRate()
void testInstantRecognitionLoanVaultExposure()
void testLoanPrincipalFromPeriodicPayment()
void testTryOverpaymentLoanInterestNoOverpaymentFees()
void testLoanPaymentDeltas()
void testLoanVaultExposureDispatcher()
void testComputePowerMinusOne()
void testCashBasisLoanVaultExposure()
void testComputeInterestAndFeeParts()
void testTryOverpaymentNoInterestOverpaymentFee()
void testLoanDefaultFreezeExemptAccounts()
void testTryOverpaymentNoInterestNoFee()
void testInstantRecognitionLoanOriginationExceedsVaultMaximum()
void testTryOverpaymentLoanInterestFeeOverpaymentInterestNoFee()
void testTryOverpaymentLoanInterestFeeOverpaymentInterestFee()
void testComputeFullPaymentInterest()
void testLoanPaymentDeltasDispatcher()
void testCashBasisLoanOriginationDeltas()
void testLoanPeriodicPayment()
void testCanApplyToBrokerCover()
void testComputeOverpaymentComponents()
void testLoanPrincipalFromPeriodicPaymentNearZeroRate()
void testTryOverpaymentLoanInterestOverpaymentInterest()
static std::shared_ptr< SLE > makeVaultSle(std::optional< VaultVersion > leVersion=std::nullopt, std::optional< Number > assetsMaximum=std::nullopt, std::optional< Number > assetsTotal=std::nullopt)
void testComputePaymentFactorNearZeroRate()
static std::shared_ptr< SLE > makeLoanSle(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
void run() override
Runs the suite.
void testComputePowerMinusOneHybrid()
void testLoanOriginationDeltasDispatcher()
void testLoanAccruedInterest()
void testLoanLatePaymentInterest()
Immutable cryptographic account descriptor.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
SLE::const_pointer le(Account const &account) const
Return an account root.
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
void disableFeature(UInt256 const feature)
void enableFeature(UInt256 const feature)
beast::Journal const journal
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Set the regular signature on a JTx.
AccountingDeltas loanPaymentDeltas(LoanPaymentParts const &parts)
AccountingDeltas loanOriginationDeltas(Number const &principalRequested)
Number loanVaultExposure(SLE::ConstRef 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)
AccountingDeltas loanPaymentDeltas(LoanPaymentParts const &parts)
Number loanVaultExposure(SLE::ConstRef loanSle)
AccountingDeltas loanOriginationDeltas(Number const &principalRequested, Number const &interestDue)
bool loanOriginationExceedsVaultMaximum(Number const &vaultMaximum, Number const &vaultTotal, Number const &interestDue)
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
json::Value set(AccountID const &account, UInt256 const &vaultId, uint32_t flags)
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
XrpT const XRP
Converts to XRP Issue or STAmount.
FeatureBitset testableAmendments()
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
bool set(T &target, std::string const &name, Section const §ion)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
bool loanOriginationExceedsVaultMaximum(SLE::ConstRef vaultSle, Number const &vaultTotal, Number const &interestDue)
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)
std::optional< LoanDefaultFreezeExemptAccounts > getLoanDefaultFreezeExemptAccounts(ReadView const &view, STTx const &tx)
Resolves the accounts and asset a LoanManage default transaction is exempt from freeze/lock for.
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
AccountingDeltas loanPaymentDeltas(SLE::ConstRef vaultSle, LoanPaymentParts const &parts)
constexpr Number abs(Number x) noexcept
TERSubset< CanCvtToTER > TER
Number loanVaultExposure(SLE::ConstRef vaultSle, SLE::ConstRef loanSle)
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)
AccountingDeltas loanOriginationDeltas(SLE::ConstRef vaultSle, Number const &principalRequested, Number const &interestDue)
TER canApplyToBrokerCover(ReadView const &view, SLE::ConstRef sleBroker, Asset const &vaultAsset, STAmount const &amount, beast::Journal j, std::string_view logPrefix)
Broker cover preclaim precision guard (fixCleanup3_2_0).
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.