50 testcase(
"cash-basis: LoanSet origination");
57 .vaultDeposit = 100'000,
64 Number const principalRequest{10'000};
74 Env env(*
this, features);
77 Account const borrower{
"borrower"};
78 env.
fund(
XRP(1'000'000), lender, borrower);
85 BEAST_EXPECT(vaultBefore && brokerBefore);
86 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
87 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
89 auto const loanSequence = brokerBefore->at(sfLoanSequence);
90 auto const loanKeylet =
93 env(
set(borrower, broker.
brokerID, xrpAsset(principalRequest).value()),
94 kCounterparty(lender),
95 kInterestRate(interestRate),
96 kPaymentTotal(paymentTotal),
97 kPaymentInterval(paymentInterval),
98 Sig(sfCounterpartySignature, lender),
103 auto const loanSle = env.
le(loanKeylet);
104 BEAST_EXPECT(loanSle);
105 Number const principalOutstanding = loanSle->at(sfPrincipalOutstanding);
106 Number const totalValueOutstanding = loanSle->at(sfTotalValueOutstanding);
107 Number const interestDue = totalValueOutstanding - principalOutstanding;
109 BEAST_EXPECT(principalOutstanding == xrpAsset(principalRequest).value());
113 BEAST_EXPECT(vaultAfter && brokerAfter);
114 Number const assetsTotalDelta =
115 Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore;
116 Number const debtTotalDelta =
Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore;
119 assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding);
123 Number principalOutstandingCash{};
125 auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] =
126 runOrigination(
all_ | featureLendingProtocolV1_1);
127 interestDueCash = interestDue;
128 principalOutstandingCash = principalOutstanding;
132 "cash-basis origination must not change AssetsTotal; delta=" +
135 debtTotalDelta == principalOutstanding,
136 "cash-basis origination must add principal-only to DebtTotal; delta=" +
141 auto const [assetsTotalDelta, debtTotalDelta, interestDue, principalOutstanding] =
142 runOrigination(
all_);
145 assetsTotalDelta == interestDue,
146 "whole-life origination must add interestDue to AssetsTotal; delta=" +
149 debtTotalDelta == principalOutstanding + interestDue,
150 "whole-life origination must add principal+interest to DebtTotal; delta=" +
157 Env env(*
this, features);
159 Account const lender{
"lender"};
160 Account const borrower{
"borrower"};
161 env.
fund(
XRP(1'000'000), lender, borrower);
167 BEAST_EXPECT(vaultSle);
168 Number const assetsTotalBefore = vaultSle->at(sfAssetsTotal);
170 Vault const vault{env};
171 auto tx = vault.set({.owner = lender, .
id = broker.
vaultID});
172 tx[sfAssetsMaximum] = assetsTotalBefore + slack;
176 env(
set(borrower, broker.
brokerID, xrpAsset(principalRequest).value()),
177 kCounterparty(lender),
178 kInterestRate(interestRate),
179 kPaymentTotal(paymentTotal),
180 kPaymentInterval(paymentInterval),
181 Sig(sfCounterpartySignature, lender),
188 Env env(*
this, features);
190 Account const lender{
"lender"};
191 Account const borrower{
"borrower"};
192 env.
fund(
XRP(1'000'000), lender, borrower);
203 env(
set(borrower, broker.
brokerID, xrpAsset(principalRequest).value()),
204 kCounterparty(lender),
205 kInterestRate(interestRate),
206 kPaymentTotal(paymentTotal),
207 kPaymentInterval(paymentInterval),
208 Sig(sfCounterpartySignature, lender),
214 Number const oneDrop = xrpAsset(1).value();
216 testcase(
"whole-life: LoanSet AssetsMaximum guard checks interestDue headroom");
225 testcase(
"cash-basis: LoanSet AssetsMaximum guard ignores interestDue headroom");
228 runVaultGuard(
all_ | featureLendingProtocolV1_1, oneDrop,
tesSUCCESS);
233 for (
auto const cashBasis : {
true,
false})
237 (cashBasis ?
"cash-basis)" :
"whole-life)"));
238 auto const features = cashBasis ?
all_ | featureLendingProtocolV1_1 :
all_;
239 Number const newDebtTotal =
240 principalOutstandingCash + (cashBasis ?
Number{} : interestDueCash);
242 runBrokerGuard(features, newDebtTotal,
tesSUCCESS);
257 using namespace loan;
258 using namespace std::chrono_literals;
263 .vaultDeposit = 1'000'000,
270 Number const principalRequest{12'000};
291 Env env(*
this, features);
293 Account const lender{
"lender"};
294 Account const borrower{
"borrower"};
295 env.
fund(
XRP(10'000'000), lender, borrower);
303 .principalRequest = principalRequest,
304 .interest = interestRate,
305 .payTotal = paymentTotal,
306 .payInterval = paymentInterval,
307 .gracePd = gracePeriod,
308 .flags = loanSetFlags,
312 BEAST_EXPECT(brokerBeforeLoan);
313 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
314 auto const loanKeylet =
317 env(loanParams(env, broker));
326 auto const loanBefore = env.
le(loanKeylet);
327 BEAST_EXPECT(vaultBefore && brokerBefore && loanBefore);
329 Number const principalBefore = loanBefore->at(sfPrincipalOutstanding);
330 Number const totalValueBefore = loanBefore->at(sfTotalValueOutstanding);
331 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
332 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
334 STAmount const amount = paymentAmount(state);
340 auto const loanAfter = env.
le(loanKeylet);
341 BEAST_EXPECT(vaultAfter && brokerAfter && loanAfter);
343 Number const principalAfter = loanAfter->at(sfPrincipalOutstanding);
344 Number const totalValueAfter = loanAfter->at(sfTotalValueOutstanding);
345 Number const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
346 Number const debtTotalAfter = brokerAfter->at(sfDebtTotal);
348 return PaymentDeltas{
349 .principalPaid = principalBefore - principalAfter,
350 .assetsTotalDelta = assetsTotalAfter - assetsTotalBefore,
351 .debtTotalDelta = debtTotalAfter - debtTotalBefore,
352 .totalValueDelta = totalValueAfter - totalValueBefore};
359 PaymentDeltas
const& off,
360 PaymentDeltas
const& on) {
361 testcase(
"cash-basis: LoanPay " + label);
366 off.principalPaid == on.principalPaid,
367 "principalPaid must be amendment-independent; off=" +
to_string(off.principalPaid) +
375 off.debtTotalDelta == off.totalValueDelta,
376 "whole-life DebtTotal delta must mirror TotalValueOutstanding delta; "
379 " totalValueDelta=" +
to_string(off.totalValueDelta));
386 Number const interestPaid =
387 off.assetsTotalDelta - off.debtTotalDelta - off.principalPaid;
390 "derived interestPaid must be non-negative: " +
to_string(interestPaid));
393 on.assetsTotalDelta == interestPaid,
394 "cash-basis AssetsTotal delta must equal interestPaid; delta=" +
397 on.debtTotalDelta == -on.principalPaid,
398 "cash-basis DebtTotal delta must equal -principalPaid; delta=" +
404 auto const noAdvance = [](
Env& env, tp
const&) { env.
close(); };
405 auto const regularAmount = [&](
LoanState const& state) {
412 auto const off = runPayment(
all_, 0, 0, noAdvance, regularAmount);
414 runPayment(
all_ | featureLendingProtocolV1_1, 0, 0, noAdvance, regularAmount);
421 "regular on-time payment must not change AssetsTotal under whole-life; delta=" +
424 checkScenario(
"regular payment", off, on);
429 auto const advancePastDue = [&](
Env& env, tp
const& startDate) {
432 auto const lateAmount = [&](
LoanState const& state) {
439 auto const off = runPayment(
all_, 0, tfLoanLatePayment, advancePastDue, lateAmount);
440 auto const on = runPayment(
441 all_ | featureLendingProtocolV1_1,
447 checkScenario(
"late payment", off, on);
452 auto const noAdvance = [](
Env& env, tp
const&) { env.
close(); };
453 auto const overpayAmount = [&](
LoanState const& state) {
459 xrpAsset(2'000).value()};
463 runPayment(
all_, tfLoanOverpayment, tfLoanOverpayment, noAdvance, overpayAmount);
464 auto const on = runPayment(
465 all_ | featureLendingProtocolV1_1,
471 checkScenario(
"overpayment", off, on);
476 auto const noAdvance = [](
Env& env, tp
const&) { env.
close(); };
477 auto const fullAmount = [&](
LoanState const&) {
481 return STAmount{xrpAsset, xrpAsset(principalRequest).value() *
Number{2}};
484 auto const off = runPayment(
all_, 0, tfLoanFullPayment, noAdvance, fullAmount);
485 auto const on = runPayment(
486 all_ | featureLendingProtocolV1_1, 0, tfLoanFullPayment, noAdvance, fullAmount);
488 checkScenario(
"full payment", off, on);
497 using namespace loan;
498 using namespace std::chrono_literals;
502 .vaultDeposit = 1'000'000,
505 .coverDeposit = 5'000,
509 Number const principalRequest{10'000};
515 auto setupLoan = [&](
Env& env) {
516 Account const lender{
"lender"};
517 Account const borrower{
"borrower"};
518 env.fund(
XRP(10'000'000), lender, borrower);
526 .principalRequest = principalRequest,
527 .interest = interestRate,
528 .payTotal = paymentTotal,
529 .payInterval = paymentInterval,
530 .gracePd = gracePeriod,
533 auto const brokerBeforeLoan = env.le(broker.
brokerKeylet());
534 BEAST_EXPECT(brokerBeforeLoan);
535 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
536 auto const loanKeylet =
539 env(loanParams(env, broker));
547 Env env(*
this, features);
548 auto const [broker, loanKeylet, lender, borrower] = setupLoan(env);
550 auto const loanBefore = env.
le(loanKeylet);
551 BEAST_EXPECT(loanBefore);
552 Number const principalOutstanding = loanBefore->at(sfPrincipalOutstanding);
553 Number const totalValueOutstanding = loanBefore->at(sfTotalValueOutstanding);
554 Number const managementFeeOutstanding = loanBefore->at(sfManagementFeeOutstanding);
556 Number const expectedExposure =
557 env.
current()->rules().enabled(featureLendingProtocolV1_1)
558 ? principalOutstanding
559 : totalValueOutstanding - managementFeeOutstanding;
561 auto const vaultBeforeImpair = env.
le(broker.vaultKeylet());
562 BEAST_EXPECT(vaultBeforeImpair);
563 Number const lossBefore = vaultBeforeImpair->at(sfLossUnrealized);
565 env(manage(lender, loanKeylet.key, tfLoanImpair),
Ter(
tesSUCCESS));
568 auto const vaultAfterImpair = env.
le(broker.vaultKeylet());
569 BEAST_EXPECT(vaultAfterImpair);
570 Number const impairDelta =
Number(vaultAfterImpair->at(sfLossUnrealized)) - lossBefore;
572 env(manage(lender, loanKeylet.key, tfLoanUnimpair),
Ter(
tesSUCCESS));
575 auto const vaultAfterUnimpair = env.
le(broker.vaultKeylet());
576 BEAST_EXPECT(vaultAfterUnimpair);
577 Number const netDelta =
Number(vaultAfterUnimpair->at(sfLossUnrealized)) - lossBefore;
582 for (
auto const features : {
all_ | featureLendingProtocolV1_1,
all_})
585 std::string(
"cash-basis: LoanManage impair/unimpair (") +
586 (features[featureLendingProtocolV1_1] ?
"enabled)" :
"disabled)"));
587 auto const [expectedExposure, impairDelta, netDelta] = runImpairUnimpair(features);
590 impairDelta == expectedExposure,
591 "impair must add loanVaultExposure to LossUnrealized; delta=" +
595 "unimpair must be an exact reversal of impair; net=" +
to_string(netDelta));
600 Env env(*
this, features);
601 auto const [broker, loanKeylet, lender, borrower] = setupLoan(env);
603 auto const loanBeforeImpair = env.
le(loanKeylet);
604 BEAST_EXPECT(loanBeforeImpair);
605 Number const principalOutstanding = loanBeforeImpair->at(sfPrincipalOutstanding);
606 Number const totalValueOutstanding = loanBeforeImpair->at(sfTotalValueOutstanding);
607 Number const managementFeeOutstanding =
608 loanBeforeImpair->at(sfManagementFeeOutstanding);
610 Number const expectedExposure =
611 env.
current()->rules().enabled(featureLendingProtocolV1_1)
612 ? principalOutstanding
613 : totalValueOutstanding - managementFeeOutstanding;
615 env(manage(lender, loanKeylet.key, tfLoanImpair),
Ter(
tesSUCCESS));
623 auto const vaultBefore = env.
le(broker.vaultKeylet());
624 auto const brokerBefore = env.
le(broker.brokerKeylet());
625 BEAST_EXPECT(vaultBefore && brokerBefore);
626 Number const assetsTotalBefore = vaultBefore->at(sfAssetsTotal);
627 Number const debtTotalBefore = brokerBefore->at(sfDebtTotal);
628 Number const lossBefore = vaultBefore->at(sfLossUnrealized);
629 Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable);
631 env(manage(lender, loanKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
634 auto const vaultAfter = env.
le(broker.vaultKeylet());
635 auto const brokerAfter = env.
le(broker.brokerKeylet());
636 BEAST_EXPECT(vaultAfter && brokerAfter);
637 Number const assetsTotalDelta =
638 Number(vaultAfter->at(sfAssetsTotal)) - assetsTotalBefore;
639 Number const debtTotalDelta =
Number(brokerAfter->at(sfDebtTotal)) - debtTotalBefore;
640 Number const lossDelta =
Number(vaultAfter->at(sfLossUnrealized)) - lossBefore;
641 Number const coverAvailableDelta =
642 Number(brokerAfter->at(sfCoverAvailable)) - coverAvailableBefore;
644 Number const defaultCovered = -coverAvailableDelta;
645 Number const vaultDefaultAmount = expectedExposure - defaultCovered;
648 expectedExposure, assetsTotalDelta, debtTotalDelta, lossDelta, vaultDefaultAmount);
651 for (
auto const features : {
all_ | featureLendingProtocolV1_1,
all_})
655 (features[featureLendingProtocolV1_1] ?
"enabled)" :
"disabled)"));
661 vaultDefaultAmount] = runDefault(features);
664 debtTotalDelta == -expectedExposure,
665 "default must reduce DebtTotal by the unified default amount; delta=" +
668 lossDelta == -expectedExposure,
669 "default must reverse the earlier impair's LossUnrealized exactly; delta=" +
672 assetsTotalDelta == -vaultDefaultAmount,
673 "default must reduce AssetsTotal by (defaultAmount - defaultCovered); delta=" +
686 testcase(
"LEVersion: legacy vault keeps accrual after amendment enabled");
689 using namespace loan;
690 using namespace std::chrono_literals;
694 .vaultDeposit = 1'000'000,
697 .coverDeposit = 5'000,
701 Number const principalRequest{10'000};
710 Account const lender{
"lender"};
711 Account const borrower{
"borrower"};
712 env.
fund(
XRP(10'000'000), lender, borrower);
719 BEAST_EXPECT(vaultSle);
720 BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion));
732 .principalRequest = principalRequest,
733 .interest = interestRate,
734 .payTotal = paymentTotal,
735 .payInterval = paymentInterval,
736 .gracePd = gracePeriod,
740 BEAST_EXPECT(brokerBeforeLoan);
741 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
747 BEAST_EXPECT(vaultBeforeSet && brokerBeforeSet);
748 Number const assetsTotalBeforeSet = vaultBeforeSet->at(sfAssetsTotal);
749 Number const debtTotalBeforeSet = brokerBeforeSet->at(sfDebtTotal);
751 env(loanParams(env, broker));
754 auto const loanAfterSet = env.
le(loanKeylet);
755 BEAST_EXPECT(loanAfterSet);
756 Number const principalOutstanding = loanAfterSet->at(sfPrincipalOutstanding);
757 Number const totalValueOutstanding = loanAfterSet->at(sfTotalValueOutstanding);
758 Number const interestDue = totalValueOutstanding - principalOutstanding;
763 BEAST_EXPECT(vaultAfterSet && brokerAfterSet);
764 Number const assetsTotalDeltaSet =
765 Number(vaultAfterSet->at(sfAssetsTotal)) - assetsTotalBeforeSet;
766 Number const debtTotalDeltaSet =
767 Number(brokerAfterSet->at(sfDebtTotal)) - debtTotalBeforeSet;
770 assetsTotalDeltaSet == interestDue,
771 "legacy vault origination must still add interestDue to AssetsTotal; delta=" +
774 debtTotalDeltaSet == principalOutstanding + interestDue,
775 "legacy vault origination must still add principal+interest to DebtTotal; delta=" +
784 auto const loanBeforePay = env.
le(loanKeylet);
785 BEAST_EXPECT(vaultBeforePay && brokerBeforePay && loanBeforePay);
786 Number const totalValueBeforePay = loanBeforePay->at(sfTotalValueOutstanding);
787 Number const assetsTotalBeforePay = vaultBeforePay->at(sfAssetsTotal);
788 Number const debtTotalBeforePay = brokerBeforePay->at(sfDebtTotal);
797 auto const loanAfterPay = env.
le(loanKeylet);
798 BEAST_EXPECT(vaultAfterPay && brokerAfterPay && loanAfterPay);
799 Number const totalValueAfterPay = loanAfterPay->at(sfTotalValueOutstanding);
800 Number const assetsTotalDeltaPay =
801 Number(vaultAfterPay->at(sfAssetsTotal)) - assetsTotalBeforePay;
802 Number const debtTotalDeltaPay =
803 Number(brokerAfterPay->at(sfDebtTotal)) - debtTotalBeforePay;
804 Number const totalValueDeltaPay = totalValueAfterPay - totalValueBeforePay;
810 "legacy vault regular payment must not change AssetsTotal; delta=" +
813 debtTotalDeltaPay == totalValueDeltaPay,
814 "legacy vault DebtTotal delta must mirror TotalValueOutstanding delta; "
819 auto const loanBeforeImpair = env.
le(loanKeylet);
820 BEAST_EXPECT(loanBeforeImpair);
821 Number const totalValueBeforeImpair = loanBeforeImpair->at(sfTotalValueOutstanding);
822 Number const managementFeeBeforeImpair = loanBeforeImpair->at(sfManagementFeeOutstanding);
823 Number const expectedExposure = totalValueBeforeImpair - managementFeeBeforeImpair;
825 env(manage(lender, loanKeylet.key, tfLoanImpair),
Ter(
tesSUCCESS));
833 auto const vaultBeforeDefault = env.
le(broker.
vaultKeylet());
835 BEAST_EXPECT(vaultBeforeDefault && brokerBeforeDefault);
836 Number const debtTotalBeforeDefault = brokerBeforeDefault->at(sfDebtTotal);
837 Number const lossBeforeDefault = vaultBeforeDefault->at(sfLossUnrealized);
839 env(manage(lender, loanKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
844 BEAST_EXPECT(vaultAfterDefault && brokerAfterDefault);
845 Number const debtTotalDeltaDefault =
846 Number(brokerAfterDefault->at(sfDebtTotal)) - debtTotalBeforeDefault;
847 Number const lossDeltaDefault =
848 Number(vaultAfterDefault->at(sfLossUnrealized)) - lossBeforeDefault;
851 debtTotalDeltaDefault == -expectedExposure,
852 "legacy vault default must reduce DebtTotal by whole-life exposure; delta=" +
855 lossDeltaDefault == -expectedExposure,
856 "legacy vault default must reverse the earlier impair's LossUnrealized exactly; "
863 BEAST_EXPECT(vaultSle);
864 BEAST_EXPECT(!vaultSle->isFieldPresent(sfLEVersion));
879 testcase(
"cash-basis: end-to-end trajectory");
882 using namespace loan;
883 using namespace std::chrono_literals;
887 .vaultDeposit = 100'000, .managementFeeRate =
TenthBips16{0}};
889 Env env(*
this,
all_ | featureLendingProtocolV1_1);
891 Account const lender{
"lender"};
892 Account const borrower{
"borrower"};
893 env.
fund(
XRP(10'000'000), lender, borrower);
908 auto const checkVaultBroker = [&](
Number const& assetsTotalUnits,
909 Number const& debtTotalUnits,
910 Number const& lossUnrealizedUnits,
911 Number const& coverAvailableUnits,
913 Number const assetsTotal = xrpAsset(assetsTotalUnits).value();
914 Number const debtTotal = xrpAsset(debtTotalUnits).value();
915 Number const lossUnrealized = xrpAsset(lossUnrealizedUnits).value();
916 Number const coverAvailable = xrpAsset(coverAvailableUnits).value();
918 auto const vaultSle = env.
le(vaultKeylet);
919 auto const brokerSle = env.
le(brokerKeylet);
920 BEAST_EXPECT(vaultSle && brokerSle);
922 vaultSle->at(sfAssetsTotal) == assetsTotal,
926 brokerSle->at(sfDebtTotal) == debtTotal,
930 vaultSle->at(sfLossUnrealized) == lossUnrealized,
934 brokerSle->at(sfCoverAvailable) == coverAvailable,
939 checkVaultBroker(100'000, 0, 0, 1'000,
"before LoanSet");
942 Number const principalRequest{1'200};
947 auto const brokerBeforeLoan = env.
le(brokerKeylet);
948 BEAST_EXPECT(brokerBeforeLoan);
949 auto const loanSequence = brokerBeforeLoan->at(sfLoanSequence);
955 .principalRequest = principalRequest,
957 .payTotal = paymentTotal,
958 .payInterval = paymentInterval,
959 .gracePd = gracePeriod,
961 env(loanParams(env, broker));
965 checkVaultBroker(100'000, 1'200, 0, 1'000,
"after LoanSet");
974 checkVaultBroker(100'000, 1'100, 0, 1'000,
"after payment 1");
979 checkVaultBroker(100'000, 1'000, 0, 1'000,
"after payment 2");
990 auto const loanBeforeDefault = env.
le(loanKeylet);
991 BEAST_EXPECT(loanBeforeDefault);
993 Number(loanBeforeDefault->at(sfPrincipalOutstanding)) == xrpAsset(1'000).value());
997 env(manage(lender, loanKeylet.key, tfLoanDefault),
Ter(
tesSUCCESS));
1000 checkVaultBroker(99'025, 0, 0, 975,
"after LoanManage(default)");