1#include <test/app/lending/LoanTestBase.h>
2#include <test/jtx/Account.h>
3#include <test/jtx/Env.h>
4#include <test/jtx/TestHelpers.h>
5#include <test/jtx/amount.h>
6#include <test/jtx/fee.h>
7#include <test/jtx/flags.h>
8#include <test/jtx/jtx_json.h>
9#include <test/jtx/noop.h>
10#include <test/jtx/pay.h>
11#include <test/jtx/ter.h>
12#include <test/jtx/trust.h>
14#include <xrpl/basics/Number.h>
15#include <xrpl/beast/unit_test/suite.h>
16#include <xrpl/beast/utility/Journal.h>
17#include <xrpl/beast/utility/Zero.h>
18#include <xrpl/core/ServiceRegistry.h>
19#include <xrpl/json/json_value.h>
20#include <xrpl/ledger/OpenView.h>
21#include <xrpl/ledger/helpers/LendingHelpers.h>
22#include <xrpl/protocol/AccountID.h>
23#include <xrpl/protocol/Feature.h>
24#include <xrpl/protocol/Indexes.h>
25#include <xrpl/protocol/Issue.h>
26#include <xrpl/protocol/LedgerFormats.h>
27#include <xrpl/protocol/Protocol.h>
28#include <xrpl/protocol/SField.h>
29#include <xrpl/protocol/STAmount.h>
30#include <xrpl/protocol/SeqProxy.h>
31#include <xrpl/protocol/TER.h>
32#include <xrpl/protocol/TxFlags.h>
33#include <xrpl/protocol/Units.h>
34#include <xrpl/tx/transactors/lending/LoanSet.h>
50 testLoanPayLateFullPaymentBypassesPenalties(
FeatureBitset features)
52 testcase(
"LoanPay full payment skips late penalties");
55 using namespace std::chrono_literals;
57 Env env(*
this, features);
61 Account const borrower{
"borrower"};
63 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
67 env(
trust(lender, asset(100'000'000)));
68 env(
trust(borrower, asset(100'000'000)));
69 env(
pay(issuer, lender, asset(50'000'000)));
70 env(
pay(issuer, borrower, asset(5'000'000)));
75 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
78 if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
81 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
84 Number const principal = asset(1'000).value();
85 Number const serviceFee = asset(2).value();
86 Number const lateFee = asset(5).value();
87 Number const closeFee = asset(4).value();
90 Sig(sfCounterpartySignature, lender),
91 kLoanServiceFee(serviceFee),
92 kLatePaymentFee(lateFee),
93 kClosePaymentFee(closeFee),
98 kPaymentInterval(600),
104 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
109 auto const overdueClose = Tp{D{state1.nextPaymentDate + state1.paymentInterval}};
110 env.
close(overdueClose);
113 auto const loanSle = env.
le(loanKeylet);
114 if (!BEAST_EXPECT(brokerSle && loanSle))
119 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
120 TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)};
121 TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)};
122 TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)};
124 Number const closePaymentFeeRounded =
126 Number const latePaymentFeeRounded =
130 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
131 Number const totalInterestOutstanding = roundedLoanState.interestDue;
133 auto const periodicRate =
loanPeriodicRate(interestRateValue, state.paymentInterval);
136 state.periodicPayment,
138 state.paymentRemaining,
141 auto const parentCloseTime = env.
current()->parentCloseTime();
142 auto const startDateSeconds =
143 static_cast<std::uint32_t>(state.startDate.time_since_epoch().count());
146 rawLoanState.principalOutstanding,
149 state.paymentInterval,
150 state.previousPaymentDate,
152 closeInterestRateValue);
154 Number const roundedFullInterestAmount =
157 broker.
asset, roundedFullInterestAmount, managementFeeRate, state.loanScale);
158 Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee;
160 Number const trackedValueDelta =
161 state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding;
162 Number const untrackedManagementFee =
163 closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding;
164 Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding;
166 Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee;
167 BEAST_EXPECT(baseFullDue ==
roundToAsset(broker.
asset, baseFullDue, state.loanScale));
169 auto const overdueSeconds =
170 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
171 if (!BEAST_EXPECT(overdueSeconds > 0))
175 Number const lateInterestRaw = state.principalOutstanding * overdueRate;
176 Number const lateInterestRounded =
179 broker.
asset, lateInterestRounded, managementFeeRate, state.loanScale);
181 lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded;
182 BEAST_EXPECT(penaltyDue >
Number{});
187 env(
pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
190 if (
auto const meta = env.
meta(); BEAST_EXPECT(meta))
191 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tesSUCCESS);
194 Number const actualPaid = balanceBefore - balanceAfter;
195 BEAST_EXPECT(actualPaid == baseFullDue);
197 Number const expectedWithPenalty = baseFullDue + penaltyDue;
198 BEAST_EXPECT(expectedWithPenalty > actualPaid);
199 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
206 testcase(
"testOverpaymentManagementFee");
209 using namespace loan;
211 Env env{*
this, features};
213 Account const lender{
"lender"}, borrower{
"borrower"};
215 env.
fund(
XRP(10'000'000), lender, borrower);
225 .vaultDeposit = asset(100'000).value(),
229 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
231 auto const brokerSle = env.
le(result.brokerKeylet());
232 if (!BEAST_EXPECT(brokerSle))
237 borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment),
238 Sig(sfCounterpartySignature, lender),
245 auto const expectedOverpaymentManagementFee =
Number{33333, 0};
246 auto const loanBrokerBalanceBefore = env.
balance(lender);
248 auto const loanPayFee =
Fee(env.
current()->fees().base * 2);
249 env(
pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee);
253 env.
balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee,
254 "overpayment management fee mismatch; expected:" +
255 to_string(expectedOverpaymentManagementFee) +
262 bool const feeCapped = features[fixCleanup3_1_3];
265 testcase <<
"DoS LoanPay: fee calculation " << (feeCapped ?
"capped" :
"uncapped");
268 using namespace std::chrono_literals;
270 Env env(*
this, features);
272 Account const issuer{
"issuer"};
273 Account const lender{
"lender"};
274 Account const borrower{
"borrower"};
276 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
279 BEAST_EXPECT(feeCapped == env.
current()->rules().enabled(fixCleanup3_1_3));
282 env(
trust(lender, iouAsset(100'000'000)));
283 env(
trust(borrower, iouAsset(100'000'000)));
284 env(
pay(issuer, lender, iouAsset(10'000'000)));
285 env(
pay(issuer, borrower, iouAsset(1'000)));
290 using namespace loan;
292 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
293 Number const principalRequest{3959'37, -2};
294 auto const baseFee = env.
current()->fees().base;
296 auto const createJson = env.
json(
308 kPaymentInterval(60),
309 kPaymentTotal(3239184));
316 env(createJson,
Sig(sfCounterpartySignature, lender));
319 auto const roundedPayment = [&]() {
321 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
322 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
326 stateBefore.periodicPayment,
327 stateBefore.loanScale,
331 auto test = [&](
int const payFactor,
335 BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184);
336 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
338 Number const amount = roundedPayment * payFactor;
340 XRPAmount const payFee{baseFee * feeFactor};
341 env(loanPayTx,
Ter(expectedTer),
Fee(payFee));
344 ?
std::min(kLoanMaximumPaymentsPerTransaction, payFactor)
349 stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange);
353 kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
359 test(1819878, 363976);
361 test(1819878, kMaxFeeIncrements, failWithoutFix);
364 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements);
366 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10);
370 test(kLoanPaymentsPerFeeIncrement * 2, 2);
375 kLoanMaximumPaymentsPerTransaction - 10,
376 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1,
379 kLoanMaximumPaymentsPerTransaction - 10,
380 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement));
383 kLoanMaximumPaymentsPerTransaction - 10,
384 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3);
386 for (
int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under)
388 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1,
telINSUF_FEE_P);
389 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements);
393 kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement,
394 kMaxFeeIncrements - 1);
396 test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements);
415 testcase(
"LoanSet near-zero interest rate covers principal");
418 using namespace loan;
420 Number const principalRequested{1000};
425 bool created =
false;
434 Env env(*
this, features);
436 Account const issuer{
"issuer"};
437 Account const lender{
"vaultOwner"};
438 Account const borrower{
"borrower"};
446 {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate =
TenthBips16{0}});
448 auto const brokerSle = env.
le(broker.brokerKeylet());
449 BEAST_EXPECT(brokerSle);
450 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
451 auto const loanKeylet =
454 env(
set(borrower, broker.brokerID, principalRequested),
455 Sig(sfCounterpartySignature, lender),
458 kPaymentInterval(400),
465 if (
auto const loanSle = env.
le(loanKeylet))
468 r.loanScale = loanSle->at(sfLoanScale);
469 r.principal = loanSle->at(sfPrincipalOutstanding);
470 r.totalValue = loanSle->at(sfTotalValueOutstanding);
471 r.managementFee = loanSle->at(sfManagementFeeOutstanding);
472 r.periodicPayment = loanSle->at(sfPeriodicPayment);
485 BEAST_EXPECT(!legacy.created);
490 BEAST_EXPECT(
fixed.created);
491 BEAST_EXPECT(
fixed.loanScale == -10);
492 BEAST_EXPECT(
fixed.principal == principalRequested);
493 BEAST_EXPECT((
fixed.totalValue ==
Number{10000000001903, -10}));
499 BEAST_EXPECT((
fixed.periodicPayment ==
Number{5000000000951293762, -16}));
500 BEAST_EXPECT((
fixed.periodicPayment * 2 ==
Number{1000000000190258752, -15}));
501 BEAST_EXPECT(
fixed.periodicPayment * 2 > principalRequested);
508 testcase <<
"Prevent nextPaymentDueDate overflow";
511 using namespace std::chrono_literals;
513 Env env{*
this, features};
515 Account const issuer{
"issuer"};
516 Account const lender{
"lender"};
517 Account const borrower{
"borrower"};
525 using namespace loan;
527 auto const loanSetFee =
Fee(env.
current()->fees().base * 2);
529 using TimeType =
decltype(sfNextPaymentDueDate)::type::value_type;
532 static_assert(kMaxTime == 4'294'967'295);
534 auto const baseJson = [&]() {
535 auto createJson = env.
json(
543 kLoanOriginationFee(218),
551 auto const baseFee = env.
current()->fees().base;
553 auto parentCloseTime = [&]() {
554 return env.
current()->parentCloseTime().time_since_epoch().count();
556 auto maxLoanTime = [&]() {
557 auto const startDate = parentCloseTime();
559 BEAST_EXPECT(startDate >= 50);
561 return kMaxTime - startDate;
566 auto const interval = maxLoanTime() + 1;
567 auto const total = 1;
568 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
576 auto const interval = 60;
577 auto const total = maxLoanTime() + 1;
578 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
586 auto const interval = maxLoanTime() + 1;
587 auto const total = 1;
588 auto const grace = interval;
589 auto createJson = env.
json(
590 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
598 auto const interval = 1'000'000'000;
599 auto const total = 10;
600 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
608 auto const interval = 60;
609 auto const total = 1'000'000'000;
610 auto createJson = env.
json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
618 auto const total = 60;
619 auto const interval = (maxLoanTime() - total) / total;
620 auto const grace = interval;
621 auto createJson = env.
json(
622 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
631 auto const grace = 100;
632 auto const interval = maxLoanTime() - grace;
633 auto const total = 1;
634 auto createJson = env.
json(
635 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
641 auto const meta = env.
meta();
642 if (BEAST_EXPECT(meta))
644 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tecKILLED);
648 auto const loanSle = env.
le(
keylet);
650 BEAST_EXPECT(!loanSle);
656 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
657 auto const grace = 5'000;
658 auto const interval = kMaxTime - closeStartDate - grace;
659 auto const total = 1;
660 auto createJson = env.
json(
661 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
667 auto const meta = env.
meta();
668 if (BEAST_EXPECT(meta))
670 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tesSUCCESS);
675 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
676 BEAST_EXPECT(afterState.previousPaymentDate == 0);
677 BEAST_EXPECT(afterState.paymentRemaining == 1);
682 env(
pay(issuer, borrower, iouAsset(
Number{1'055'524'81, -2})));
685 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
686 auto const grace = 5'000;
687 auto const maxLoanTime = kMaxTime - closeStartDate - grace;
688 auto const total = [&]() {
689 if (maxLoanTime % 5 == 0)
691 if (maxLoanTime % 3 == 0)
693 if (maxLoanTime % 2 == 0)
697 if (!BEAST_EXPECT(total != 0))
701 if (!BEAST_EXPECT(brokerState))
704 auto const loanSequence = brokerState->at(sfLoanSequence);
707 auto const interval = maxLoanTime / total;
708 auto createJson = env.
json(
709 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
716 BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval);
717 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
718 BEAST_EXPECT(beforeState.paymentRemaining == total);
719 BEAST_EXPECT(beforeState.periodicPayment > 0);
724 Number const payment = beforeState.periodicPayment * (total - 1);
725 XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)};
728 auto loanPayTx = env.
json(
pay(borrower,
keylet.key, paymentAmount),
Fee(payFee));
735 BEAST_EXPECT(afterState.paymentRemaining == 1);
736 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
737 BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval);
770 Account const issuer{
"issuer"};
771 Account const lender{
"lender"};
772 Account const borrower{
"borrower"};
775 bool const pseudoExempt = features[fixCleanup3_4_0];
780 auto const payeeLabel = [payee]() ->
char const* {
786 return "vault authorized by the issuer";
788 return "loan broker";
793 testcase <<
"LoanPay crediting an unauthorized " << payeeLabel <<
": pseudo-account "
794 << (pseudoExempt ?
"exempt" :
"not exempt");
796 Env env{*
this, features};
798 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
801 env(
fset(issuer, asfRequireAuth));
805 env(
trust(lender, asset(100'000'000)));
806 env(
trust(borrower, asset(100'000'000)));
811 env(
trust(issuer, asset(0), lender, tfSetfAuth));
812 env(
trust(issuer, asset(0), borrower, tfSetfAuth));
815 env(
pay(issuer, lender, asset(10'000'000)));
816 env(
pay(issuer, borrower, asset(10'000)));
823 auto const vaultSle = env.le(broker.
vaultKeylet());
825 if (!BEAST_EXPECT(vaultSle && brokerSle))
828 Account const vaultPseudo{
"vault pseudo-account", vaultSle->at(sfAccount)};
829 Account const brokerPseudo{
"broker pseudo-account", brokerSle->at(sfAccount)};
831 auto const lineIsAuthorized = [&](
Account const& holder) ->
bool {
833 if (!BEAST_EXPECT(line))
835 return line->isFlag(holder.id() > issuer.
id() ? lsfLowAuth : lsfHighAuth);
838 BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
839 BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
843 env(
trust(issuer, asset(0), vaultPseudo, tfSetfAuth));
845 BEAST_EXPECT(lineIsAuthorized(vaultPseudo));
848 using namespace loan;
852 Number const serviceFee = asset(2).value();
854 env(
set(borrower, broker.
brokerID, asset(1'000).value()),
855 Sig(sfCounterpartySignature, lender),
856 kLoanServiceFee(serviceFee),
859 kPaymentInterval(600),
860 Fee(env.current()->fees().base * 2));
864 BEAST_EXPECT(env.le(loanKeylet));
870 env(
trust(issuer, asset(0), lender, tfSetFreeze | tfSetDeepFreeze));
878 broker.
asset, state.periodicPayment + serviceFee, state.loanScale)};
883 auto const assetsAvailable = [&]() ->
Number {
885 if (!BEAST_EXPECT(sle))
887 return sle->at(sfAssetsAvailable);
890 auto const borrowerBefore = env.balance(borrower, asset).number();
891 auto const vaultBefore = env.balance(vaultPseudo, asset).number();
892 auto const brokerBefore = env.balance(brokerPseudo, asset).number();
893 auto const assetsAvailableBefore = assetsAvailable();
895 env(
pay(borrower, loanKeylet.key, payment),
901 BEAST_EXPECT(env.balance(borrower, asset).number() < borrowerBefore);
902 BEAST_EXPECT(env.balance(vaultPseudo, asset).number() > vaultBefore);
903 BEAST_EXPECT(assetsAvailable() > assetsAvailableBefore);
907 (env.balance(brokerPseudo, asset).number() > brokerBefore) ==
913 BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
915 BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
920 BEAST_EXPECT(env.balance(borrower, asset).number() == borrowerBefore);
921 BEAST_EXPECT(env.balance(vaultPseudo, asset).number() == vaultBefore);
922 BEAST_EXPECT(env.balance(brokerPseudo, asset).number() == brokerBefore);
923 BEAST_EXPECT(assetsAvailable() == assetsAvailableBefore);
927 for (
auto const& features : {
all_,
all_ - fixCleanup3_4_0})
945 testcase(
"LoanPay funds conserved: broker fee payee below reserve");
949 Env env(*
this, features);
951 Account const issuer{
"issuer"};
952 Account const lender{
"lender"};
953 Account const borrower{
"borrower"};
959 Number const serviceFeeValue{2};
963 .principalRequest = 1000,
964 .serviceFee = serviceFeeValue,
967 .payInterval = 3600};
971 if (BEAST_EXPECT(loanOpt); !loanOpt.has_value())
973 auto const& [broker, loanKeylet, brokerPseudo] = *loanOpt;
975 auto const vaultPseudo = [&]() {
977 if (!BEAST_EXPECT(vaultSle))
979 return vaultSle->at(sfAccount);
986 if (!BEAST_EXPECT(sle))
988 return sle->getFieldAmount(sfBalance);
990 auto lenderReserve = [&] {
1000 env(
pay(lender, issuer, rawBalance(lender.
id()) - lenderReserve() - baseFee));
1003 BEAST_EXPECT(env.
balance(lender) < lenderReserve());
1008 STAmount const roundedPeriodicPayment{
1014 auto const borrowerBefore = rawBalance(borrower.
id());
1015 auto const vaultBefore = rawBalance(vaultPseudo);
1016 auto const lenderBefore = rawBalance(lender.
id());
1021 env(
loan::pay(borrower, loanKeylet.key, totalDue));
1024 auto const borrowerAfter = rawBalance(borrower.
id());
1025 auto const vaultAfter = rawBalance(vaultPseudo);
1026 auto const lenderAfter = rawBalance(lender.
id());
1030 BEAST_EXPECT(lenderAfter > lenderBefore);
1031 BEAST_EXPECT(lenderAfter < lenderReserve());
1035 borrowerBefore - baseFee + vaultBefore + lenderBefore ==
1036 borrowerAfter + vaultAfter + lenderAfter);
1053 using namespace jtx;
1055 auto const sle = view.
read(loanKeylet);
1059 (*replacement)[sfNextPaymentDueDate] = dueDate;
1075 testcase(
"LoanPay at exact due date succeeds with fixCleanup3_4_0");
1077 using namespace jtx;
1078 using namespace loan;
1081 BEAST_EXPECT(env.
enabled(fixCleanup3_4_0));
1083 Account const lender{
"lender"};
1084 Account const borrower{
"borrower"};
1086 env.
fund(
XRP(10'000'000), lender, borrower);
1093 if (!BEAST_EXPECT(brokerSle))
1095 auto const loanKeylet =
1102 env(
set(borrower, broker.brokerID, asset(1'000).value()),
1103 Sig(sfCounterpartySignature, lender),
1105 kPaymentInterval(600),
1107 kLatePaymentFee(asset(50).value()),
1112 BEAST_EXPECT(stateBefore.paymentRemaining == 12);
1114 STAmount const roundedPeriodicPayment{
1120 env.
current()->parentCloseTime().time_since_epoch().count();
1124 auto const borrowerBefore = env.
balance(borrower, asset).
number();
1138 auto const borrowerAfter = env.
balance(borrower, asset).
number();
1144 Number const charged = borrowerBefore - borrowerAfter -
Number{payFee};
1145 BEAST_EXPECT(charged >
Number{});
1146 BEAST_EXPECT(charged <=
Number{roundedPeriodicPayment});
1149 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining - 1);
1150 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate + stateBefore.paymentInterval);
1162 testcase(
"LoanPay at exact due date fails without fixCleanup3_4_0");
1164 using namespace jtx;
1165 using namespace loan;
1167 Env env(*
this,
all_ - fixCleanup3_4_0);
1168 BEAST_EXPECT(!env.
enabled(fixCleanup3_4_0));
1170 Account const lender{
"lender"};
1171 Account const borrower{
"borrower"};
1173 env.
fund(
XRP(10'000'000), lender, borrower);
1180 if (!BEAST_EXPECT(brokerSle))
1182 auto const loanKeylet =
1185 env(
set(borrower, broker.brokerID, asset(1'000).value()),
1186 Sig(sfCounterpartySignature, lender),
1188 kPaymentInterval(600),
1193 BEAST_EXPECT(stateBefore.paymentRemaining == 12);
1195 STAmount const roundedPeriodicPayment{
1201 env.
current()->parentCloseTime().time_since_epoch().count();
1213 env(
pay(borrower, loanKeylet.key, roundedPeriodicPayment),
Ter(
tecEXPIRED));
1216 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
1217 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
1228 testcase(
"LoanPay(tfLoanLatePayment) at exact due date rejected with fixCleanup3_4_0");
1230 using namespace jtx;
1231 using namespace loan;
1234 BEAST_EXPECT(env.
enabled(fixCleanup3_4_0));
1236 Account const lender{
"lender"};
1237 Account const borrower{
"borrower"};
1239 env.
fund(
XRP(10'000'000), lender, borrower);
1246 if (!BEAST_EXPECT(brokerSle))
1248 auto const loanKeylet =
1251 env(
set(borrower, broker.brokerID, asset(1'000).value()),
1252 Sig(sfCounterpartySignature, lender),
1254 kPaymentInterval(600),
1256 kLatePaymentFee(asset(50).value()),
1261 BEAST_EXPECT(stateBefore.paymentRemaining == 12);
1273 env.
current()->parentCloseTime().time_since_epoch().count();
1284 env(
pay(borrower, loanKeylet.key, generousAmount, tfLoanLatePayment),
Ter(
tecTOO_SOON));
1287 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
1288 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
1299 testcase(
"LoanPay catch-up fee at exact due date with fixCleanup3_4_0");
1301 using namespace jtx;
1302 using namespace loan;
1306 BEAST_EXPECT(env.
enabled(fixCleanup3_4_0));
1308 Account const lender{
"lender"};
1309 Account const borrower{
"borrower"};
1311 env.
fund(
XRP(10'000'000), lender, borrower);
1318 if (!BEAST_EXPECT(brokerSle))
1320 auto const loanKeylet =
1323 env(
set(borrower, broker.brokerID, asset(10'000).value()),
1324 Sig(sfCounterpartySignature, lender),
1326 kPaymentInterval(600),
1331 BEAST_EXPECT(stateBefore.paymentRemaining == 50);
1332 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanPaymentsPerFeeIncrement);
1334 auto const loanSle = env.
le(loanKeylet);
1335 if (!BEAST_EXPECT(loanSle))
1337 Number const regularPayment =
1339 loanSle->at(sfLoanServiceFee);
1340 int const payCount = kLoanPaymentsPerFeeIncrement * 4;
1341 STAmount const catchUp{asset, regularPayment * payCount};
1343 XRPAmount const escalatedFee{baseFee * (payCount / kLoanPaymentsPerFeeIncrement)};
1346 env.
current()->parentCloseTime().time_since_epoch().count();
1359 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining - payCount);
1368 testcase(
"LoanPay catch-up fee at exact due date without fixCleanup3_4_0");
1370 using namespace jtx;
1371 using namespace loan;
1374 Env env(*
this,
all_ - fixCleanup3_4_0);
1375 BEAST_EXPECT(!env.
enabled(fixCleanup3_4_0));
1377 Account const lender{
"lender"};
1378 Account const borrower{
"borrower"};
1380 env.
fund(
XRP(10'000'000), lender, borrower);
1387 if (!BEAST_EXPECT(brokerSle))
1389 auto const loanKeylet =
1392 env(
set(borrower, broker.brokerID, asset(10'000).value()),
1393 Sig(sfCounterpartySignature, lender),
1395 kPaymentInterval(600),
1400 BEAST_EXPECT(stateBefore.paymentRemaining == 50);
1402 auto const loanSle = env.
le(loanKeylet);
1403 if (!BEAST_EXPECT(loanSle))
1405 Number const regularPayment =
1407 loanSle->at(sfLoanServiceFee);
1408 int const payCount = kLoanPaymentsPerFeeIncrement * 4;
1409 STAmount const catchUp{asset, regularPayment * payCount};
1413 env.
current()->parentCloseTime().time_since_epoch().count();
1419 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
1420 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
1430 using namespace jtx;
1431 using namespace loan;
1436 "LoanPay broker-owner borrower existing line after "
1437 "issuer clears asfDefaultRipple (") +
1438 (features[fixCleanup3_4_0] ?
"post" :
"pre") +
"-fixCleanup3_4_0)");
1440 Env env(*
this, features);
1441 Account const issuer{
"issuer"};
1444 env.
fund(
XRP(10'000), issuer, alice);
1446 env(
fset(issuer, asfDefaultRipple));
1450 env(
trust(alice, usd(10'000'000)));
1452 env(
pay(issuer, alice, usd(2'000'000)));
1457 if (!BEAST_EXPECT(brokerSle))
1459 auto const loanKeylet =
1462 Number const serviceFee = usd(2).value();
1463 env(
set(alice, broker.brokerID, usd(1'000).value()),
1464 Sig(sfCounterpartySignature, alice),
1465 kLoanServiceFee(serviceFee),
1469 env(
fclear(issuer, asfDefaultRipple));
1478 env(
pay(alice, loanKeylet.key, payment),
Ter(expected));
1504 testLoanPayLateFullPaymentBypassesPenalties(features);
1518 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2},
all_))
A generic endpoint for log messages.
TestcaseT testcase
Memberspace for declaring test cases.
Value removeMember(char const *key)
Remove and return the named member.
constexpr TIss const & get() const
A currency issued by an account.
static constexpr std::uint32_t kDefaultGracePeriod
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.
bool modify(ModifyType const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
void rawReplace(SLE::Ref sle) override
Unconditionally replace a state item.
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Asset const & asset() const
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
virtual OpenLedger & getOpenLedger()=0
void run() override
Runs the suite.
void testLoanPayAtExactDueDateSucceedsPostAmendment()
void testRepayIntoUnauthorizedVault()
void runAmendmentIndependent()
void testDosLoanPay(FeatureBitset features)
void testLoanPayFundsConservedPayeeBelowReserve(FeatureBitset features)
void setLoanNextPaymentDueDate(jtx::Env &env, Keylet const &loanKeylet, std::uint32_t dueDate)
void testLoanLatePaymentAtExactDueDateRejectedPostAmendment()
void testLoanSetNearZeroInterestRateSucceeds()
void testLoanPayAtExactDueDateFailsPreAmendment()
void testLoanPaySelfBrokerExistingLineDefaultRipple()
void runAmendmentSensitive(FeatureBitset features)
void testLoanNextPaymentDueDateOverflow(FeatureBitset features)
void testOverpaymentManagementFee(FeatureBitset features)
void testLoanPayCatchUpFeeAtExactDueDatePostAmendment()
void testLoanPayCatchUpFeeAtExactDueDatePreAmendment()
Shared base for the Loan*_test family under src/test/app/lending/.
jtx::PrettyAsset createFundedIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=100 '000 '000, Number const &borrowerPay=1 '000 '000)
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const ¶ms=BrokerParameters::defaults())
LoanState getCurrentState(jtx::Env const &env, BrokerInfo const &broker, Keylet const &loanKeylet)
Get the state without checking anything.
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
std::optional< std::tuple< BrokerInfo, Keylet, jtx::Account > > createLoan(jtx::Env &env, AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
static jtx::PrettyAsset createFundedRippleIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=1 '000 '000, Number const &borrowerPay=1 '000 '000)
std::string const iouCurrency_
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.
TER ter() const
Return the TER for the last JTx.
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
SLE::const_pointer le(Account const &account) const
Return an account root.
bool enabled(UInt256 feature) const
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Set the regular signature on a JTx.
Set the expected result code for a JTx The test will fail if the code doesn't match.
@ Object
object value (collection of name/value pairs).
Keylet computation functions.
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Keylet account(AccountID const &id) noexcept
AccountID root.
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const ¤cy) noexcept
The index of a trust line for a given currency.
json::Value set(AccountID const &account, UInt256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
auto const kPaymentInterval
auto const kOverpaymentInterestRate
json::Value pay(AccountID const &account, UInt256 const &loanID, STAmount const &amount, std::uint32_t flags)
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< UInt256 > features, FeatureBitset seed)
Returns all 2^N permutations of a seed FeatureBitset with each subset of the given features excluded.
XrpT const XRP
Converts to XRP Issue or STAmount.
json::Value noop(Account const &account)
The null transaction.
std::uint32_t ownerCount(Env const &env, Account const &account)
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
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)
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
TenthBips< std::uint32_t > TenthBips32
static FunctionType fixed(Keylet const &keylet)
TenthBips< std::uint16_t > TenthBips16
boost::outcome_v2::result< T, std::error_code > Result
std::string to_string(BaseUInt< Bits, Tag > const &a)
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::RoundingMode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
bool isTesSuccess(TER x) noexcept
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
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)
A pair of SHAMap key and LedgerEntryType.
Keylet brokerKeylet() const
Keylet vaultKeylet() const
Asset const & raw() const