xrpld
Loading...
Searching...
No Matches
LoanPay_test.cpp
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/jtx_json.h>
8#include <test/jtx/pay.h>
9#include <test/jtx/ter.h>
10#include <test/jtx/trust.h>
11
12#include <xrpl/basics/Number.h>
13#include <xrpl/beast/unit_test/suite.h>
14#include <xrpl/beast/utility/Zero.h>
15#include <xrpl/json/json_value.h>
16#include <xrpl/protocol/Feature.h>
17#include <xrpl/protocol/Indexes.h>
18#include <xrpl/protocol/Issue.h>
19#include <xrpl/protocol/Protocol.h>
20#include <xrpl/protocol/SField.h>
21#include <xrpl/protocol/STAmount.h>
22#include <xrpl/protocol/SeqProxy.h>
23#include <xrpl/protocol/TER.h>
24#include <xrpl/protocol/TxFlags.h>
25#include <xrpl/protocol/Units.h>
26#include <xrpl/tx/transactors/lending/LoanSet.h>
27
28#include <algorithm>
29#include <cstdint>
30#include <limits>
31#include <type_traits>
32
33namespace xrpl::test {
34
36{
37private:
38#if LOAN_TODO
39 void
40 testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features)
41 {
42 testcase("LoanPay full payment skips late penalties");
43 using namespace jtx;
44 using namespace loan;
45 using namespace std::chrono_literals;
46
47 Env env(*this, features);
48
49 Account const issuer{"issuer"};
50 Account const lender{"lender"};
51 Account const borrower{"borrower"};
52
53 env.fund(XRP(1'000'000), issuer, lender, borrower);
54 env.close();
55
56 PrettyAsset const asset = issuer[iouCurrency];
57 env(trust(lender, asset(100'000'000)));
58 env(trust(borrower, asset(100'000'000)));
59 env(pay(issuer, lender, asset(50'000'000)));
60 env(pay(issuer, borrower, asset(5'000'000)));
61 env.close();
62
63 BrokerInfo broker{createVaultAndBroker(env, asset, lender)};
64
65 auto const loanSetFee = Fee(env.current()->fees().base * 2);
66
67 auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID));
68 if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
69 return;
70
71 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
72 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
73
74 Number const principal = asset(1'000).value();
75 Number const serviceFee = asset(2).value();
76 Number const lateFee = asset(5).value();
77 Number const closeFee = asset(4).value();
78
79 env(set(borrower, broker.brokerID, principal),
80 Sig(sfCounterpartySignature, lender),
81 kLoanServiceFee(serviceFee),
82 kLatePaymentFee(lateFee),
83 kClosePaymentFee(closeFee),
84 kInterestRate(percentageToTenthBips(12)),
85 kLateInterestRate(percentageToTenthBips(24) / 10),
86 kCloseInterestRate(percentageToTenthBips(5)),
87 kPaymentTotal(12),
88 kPaymentInterval(600),
89 kGracePeriod(0),
90 Fee(loanSetFee));
91 env.close();
92
93 auto state1 = getCurrentState(env, broker, loanKeylet);
94 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
95 return;
96
97 using d = NetClock::duration;
98 using tp = NetClock::time_point;
99 auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}};
100 env.close(overdueClose);
101
102 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
103 auto const loanSle = env.le(loanKeylet);
104 if (!BEAST_EXPECT(brokerSle && loanSle))
105 return;
106
107 auto state = getCurrentState(env, broker, loanKeylet);
108
109 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
110 TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)};
111 TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)};
112 TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)};
113
114 Number const closePaymentFeeRounded =
115 roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale);
116 Number const latePaymentFeeRounded =
117 roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale);
118
119 auto const roundedLoanState = constructLoanState(
120 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
121 Number const totalInterestOutstanding = roundedLoanState.interestDue;
122
123 auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval);
124 auto const rawLoanState = computeTheoreticalLoanState(
125 env.current()->rules(),
126 state.periodicPayment,
127 periodicRate,
128 state.paymentRemaining,
129 managementFeeRate);
130
131 auto const parentCloseTime = env.current()->parentCloseTime();
132 auto const startDateSeconds =
133 static_cast<std::uint32_t>(state.startDate.time_since_epoch().count());
134
135 Number const fullPaymentInterest = computeFullPaymentInterest(
136 rawLoanState.principalOutstanding,
137 periodicRate,
138 parentCloseTime,
139 state.paymentInterval,
140 state.previousPaymentDate,
141 startDateSeconds,
142 closeInterestRateValue);
143
144 Number const roundedFullInterestAmount =
145 roundToAsset(broker.asset, fullPaymentInterest, state.loanScale);
146 Number const roundedFullManagementFee = computeManagementFee(
147 broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale);
148 Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee;
149
150 Number const trackedValueDelta =
151 state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding;
152 Number const untrackedManagementFee =
153 closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding;
154 Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding;
155
156 Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee;
157 BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale));
158
159 auto const overdueSeconds =
160 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
161 if (!BEAST_EXPECT(overdueSeconds > 0))
162 return;
163
164 Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds);
165 Number const lateInterestRaw = state.principalOutstanding * overdueRate;
166 Number const lateInterestRounded =
167 roundToAsset(broker.asset, lateInterestRaw, state.loanScale);
168 Number const lateManagementFeeRounded = computeManagementFee(
169 broker.asset, lateInterestRounded, managementFeeRate, state.loanScale);
170 Number const penaltyDue =
171 lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded;
172 BEAST_EXPECT(penaltyDue > Number{});
173
174 auto const balanceBefore = env.balance(borrower, broker.asset).number();
175
176 STAmount const paymentAmount{broker.asset.raw(), baseFullDue};
177 env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
178 env.close();
179
180 if (auto const meta = env.meta(); BEAST_EXPECT(meta))
181 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
182
183 auto const balanceAfter = env.balance(borrower, broker.asset).number();
184 Number const actualPaid = balanceBefore - balanceAfter;
185 BEAST_EXPECT(actualPaid == baseFullDue);
186
187 Number const expectedWithPenalty = baseFullDue + penaltyDue;
188 BEAST_EXPECT(expectedWithPenalty > actualPaid);
189 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
190 }
191#endif
192
193 void
195 {
196 testcase("testOverpaymentManagementFee");
197
198 using namespace jtx;
199 using namespace loan;
200
201 Env env{*this, features};
202
203 Account const lender{"lender"}, borrower{"borrower"};
204
205 env.fund(XRP(10'000'000), lender, borrower);
206 env.close();
207
208 PrettyAsset const asset{xrpIssue(), 1000};
209
210 auto const result = createVaultAndBroker(
211 env,
212 asset,
213 lender,
214 {
215 .vaultDeposit = asset(100'000).value(),
216 .managementFeeRate = TenthBips16(10'000),
217 });
218
219 auto const loanSetFee = Fee(env.current()->fees().base * 2);
220
221 auto const brokerSle = env.le(result.brokerKeylet());
222 if (!BEAST_EXPECT(brokerSle))
223 return;
224 auto const loanKeylet = keylet::loan(
225 result.brokerKeylet().key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
226 env(loan::set(
227 borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment),
228 Sig(sfCounterpartySignature, lender),
229 loan::kPaymentInterval(86400 * 30),
232 loanSetFee);
233
234 // From calculator
235 auto const expectedOverpaymentManagementFee = Number{33333, 0};
236 auto const loanBrokerBalanceBefore = env.balance(lender);
237
238 auto const loanPayFee = Fee(env.current()->fees().base * 2);
239 env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee);
240 env.close();
241
242 BEAST_EXPECTS(
243 env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee,
244 "overpayment management fee mismatch; expected:" +
245 to_string(expectedOverpaymentManagementFee) +
246 " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore));
247 }
248
249 void
251 {
252 bool const feeCapped = features[fixCleanup3_1_3];
253
254 // From FIND-005
255 testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped");
256
257 using namespace jtx;
258 using namespace std::chrono_literals;
259 using namespace lending;
260 Env env(*this, features);
261
262 Account const issuer{"issuer"};
263 Account const lender{"lender"};
264 Account const borrower{"borrower"};
265
266 env.fund(XRP(1'000'000), issuer, lender, borrower);
267 env.close();
268
269 BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3));
270
271 PrettyAsset const iouAsset = issuer[iouCurrency_];
272 env(trust(lender, iouAsset(100'000'000)));
273 env(trust(borrower, iouAsset(100'000'000)));
274 env(pay(issuer, lender, iouAsset(10'000'000)));
275 env(pay(issuer, borrower, iouAsset(1'000)));
276 env.close();
277
278 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
279
280 using namespace loan;
281
282 auto const loanSetFee = Fee(env.current()->fees().base * 2);
283 Number const principalRequest{3959'37, -2};
284 auto const baseFee = env.current()->fees().base;
285
286 auto const createJson = env.json(
287 set(borrower, broker.brokerID, principalRequest),
288 Fee(loanSetFee),
289 Json(sfCounterpartySignature, json::ValueType::Object),
290 kClosePaymentFee(0),
291 kGracePeriod(60),
292 kInterestRate(TenthBips32(20930)),
293 kLateInterestRate(TenthBips32(77049)),
294 kLatePaymentFee(0),
295 kLoanServiceFee(0),
296 kOverpaymentFee(TenthBips32(7)),
297 kOverpaymentInterestRate(TenthBips32(66653)),
298 kPaymentInterval(60),
299 kPaymentTotal(3239184));
300
301 // There are enough payments due on this loan that it only needs to be
302 // created once, and can be paid on multiple times. Just don't create a
303 // gazillion test cases.
304 auto const keylet = nextLoanKeylet(env, broker);
305
306 env(createJson, Sig(sfCounterpartySignature, lender));
307 env.close();
308
309 auto const roundedPayment = [&]() {
310 auto const stateBefore = getCurrentState(env, broker, keylet);
311 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
312 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
313
314 return roundToAsset(
315 iouAsset,
316 stateBefore.periodicPayment,
317 stateBefore.loanScale,
319 }();
320
321 auto test = [&](int const payFactor,
322 int const feeFactor,
323 TER const expectedTer = tesSUCCESS) {
324 auto const stateBefore = getCurrentState(env, broker, keylet);
325 BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184);
326 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
327
328 Number const amount = roundedPayment * payFactor;
329 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount}));
330 XRPAmount const payFee{baseFee * feeFactor};
331 env(loanPayTx, Ter(expectedTer), Fee(payFee));
332 env.close();
333 auto const expectedChange = isTesSuccess(expectedTer)
334 ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor)
335 : 0;
336
337 auto const stateAfter = getCurrentState(env, broker, keylet);
338 BEAST_EXPECT(
339 stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange);
340 };
341
342 static constexpr std::int64_t kMaxFeeIncrements =
343 kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
344
345 TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P;
346
347 // * Amount well above threshold -> capped fee
348 // The original test case - way over the limit - more fee is always ok
349 test(1819878, 363976);
350 // The capped fee is only sufficient if the amendment is enabled.
351 test(1819878, kMaxFeeIncrements, failWithoutFix);
352
353 // * Amount exactly at threshold -> capped fee
354 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements);
355 // More fee is always ok
356 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10);
357
358 // * Amount below threshold -> normal calculation
359 test(1, 1);
360 test(kLoanPaymentsPerFeeIncrement * 2, 2);
361 test(0, 0, temBAD_AMOUNT);
362 test(0, 1, temBAD_AMOUNT);
363 // Fee difference rounds evenly
364 test(
365 kLoanMaximumPaymentsPerTransaction - 10,
366 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1,
368 test(
369 kLoanMaximumPaymentsPerTransaction - 10,
370 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement));
371 // More fee is always ok
372 test(
373 kLoanMaximumPaymentsPerTransaction - 10,
374 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3);
375 // Fee rounds up
376 for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under)
377 {
378 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P);
379 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements);
380 }
381 // Only when you get one less fee increment can you pay less
382 test(
383 kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement,
384 kMaxFeeIncrements - 1);
385 // And again, more fee is always ok.
386 test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements);
387 }
388
389 // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero
390 // rate). At such a near-zero rate the closed-form payment factor
391 // (1 + r)^n - 1 cancels catastrophically.
392 //
393 // Without fixCleanup3_2_0 the resulting amortization is degenerate and the
394 // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the
395 // amendment, computePowerMinusOneHybrid uses a numerically-stable series
396 // expansion, so the loan is created and the scheduled payments
397 // (2 * periodicPayment) cover the principal — no economic underpayment
398 // (yield theft).
399 //
400 // The test runs the same LoanSet under both amendment settings and pins the
401 // exact outcome for each.
402 void
404 {
405 testcase("LoanSet near-zero interest rate covers principal");
406
407 using namespace jtx;
408 using namespace loan;
409
410 Number const principalRequested{1000};
411
412 struct Result
413 {
414 TER ter = tesSUCCESS;
415 bool created = false;
416 std::int32_t loanScale = 0;
417 Number principal;
418 Number totalValue;
419 Number managementFee;
420 Number periodicPayment;
421 };
422
423 auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result {
424 Env env(*this, features);
425
426 Account const issuer{"issuer"};
427 Account const lender{"vaultOwner"};
428 Account const borrower{"borrower"};
429
430 PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower);
431
432 auto const broker = createVaultAndBroker(
433 env,
434 iouAsset,
435 lender,
436 {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}});
437
438 auto const brokerSle = env.le(broker.brokerKeylet());
439 BEAST_EXPECT(brokerSle);
440 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
441 auto const loanKeylet =
442 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
443
444 env(set(borrower, broker.brokerID, principalRequested),
445 Sig(sfCounterpartySignature, lender),
446 kInterestRate(TenthBips32{1}),
447 kPaymentTotal(2),
448 kPaymentInterval(400),
449 Fee(env.current()->fees().base * 2),
450 Ter(expectedTer));
451 env.close();
452
453 Result r;
454 r.ter = env.ter();
455 if (auto const loanSle = env.le(loanKeylet))
456 {
457 r.created = true;
458 r.loanScale = loanSle->at(sfLoanScale);
459 r.principal = loanSle->at(sfPrincipalOutstanding);
460 r.totalValue = loanSle->at(sfTotalValueOutstanding);
461 r.managementFee = loanSle->at(sfManagementFeeOutstanding);
462 r.periodicPayment = loanSle->at(sfPeriodicPayment);
463 }
464 return r;
465 };
466
467 Result const fixed = runScenario(all_, tesSUCCESS);
468 Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS);
469
470 // Without the amendment, the catastrophically-cancelling closed-form
471 // payment factor produces a degenerate amortization that fails
472 // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no
473 // loan is created.
474 BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS);
475 BEAST_EXPECT(!legacy.created);
476
477 // With the amendment the stable series expansion produces a valid loan
478 // at loanScale -10.
479 BEAST_EXPECT(fixed.ter == tesSUCCESS);
480 BEAST_EXPECT(fixed.created);
481 BEAST_EXPECT(fixed.loanScale == -10);
482 BEAST_EXPECT(fixed.principal == principalRequested);
483 BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10}));
484 BEAST_EXPECT(fixed.managementFee == beast::kZero);
485
486 // Periodic payment from the numerically-stable series expansion, and the
487 // scheduled total (2 * periodicPayment) which exceeds the 1000 principal
488 // — no economic underpayment / yield theft.
489 BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16}));
490 BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15}));
491 BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested);
492 }
493
494 void
496 {
497 // For FIND-013
498 testcase << "Prevent nextPaymentDueDate overflow";
499
500 using namespace jtx;
501 using namespace std::chrono_literals;
502 using namespace lending;
503 Env env{*this, features};
504
505 Account const issuer{"issuer"};
506 Account const lender{"lender"};
507 Account const borrower{"borrower"};
508
509 PrettyAsset const iouAsset =
510 createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000);
511
512 BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}};
513 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
514
515 using namespace loan;
516
517 auto const loanSetFee = Fee(env.current()->fees().base * 2);
518
519 using timeType = decltype(sfNextPaymentDueDate)::type::value_type;
521 constexpr timeType kMaxTime = std::numeric_limits<timeType>::max();
522 static_assert(kMaxTime == 4'294'967'295);
523
524 auto const baseJson = [&]() {
525 auto createJson = env.json(
526 set(borrower, broker.brokerID, Number{55524'81, -2}),
527 Fee(loanSetFee),
528 kClosePaymentFee(0),
529 kGracePeriod(LoanSet::kDefaultGracePeriod),
530 kInterestRate(TenthBips32(12833)),
531 kLateInterestRate(TenthBips32(77048)),
532 kLatePaymentFee(0),
533 kLoanOriginationFee(218),
534 Json(sfCounterpartySignature, json::ValueType::Object));
535
536 createJson.removeMember(sfSequence.getJsonName());
537
538 return createJson;
539 }();
540
541 auto const baseFee = env.current()->fees().base;
542
543 auto parentCloseTime = [&]() {
544 return env.current()->parentCloseTime().time_since_epoch().count();
545 };
546 auto maxLoanTime = [&]() {
547 auto const startDate = parentCloseTime();
548
549 BEAST_EXPECT(startDate >= 50);
550
551 return kMaxTime - startDate;
552 };
553
554 {
555 // straight-up overflow: interval
556 auto const interval = maxLoanTime() + 1;
557 auto const total = 1;
558 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
559
560 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
561 env.close();
562 }
563 {
564 // straight-up overflow: total
565 // min interval is 60
566 auto const interval = 60;
567 auto const total = maxLoanTime() + 1;
568 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
569
570 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
571 env.close();
572 }
573 {
574 // straight-up overflow: grace period
575 // min interval is 60
576 auto const interval = maxLoanTime() + 1;
577 auto const total = 1;
578 auto const grace = interval;
579 auto createJson = env.json(
580 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
581
582 // The grace period can't be larger than the interval.
583 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
584 env.close();
585 }
586 {
587 // Overflow with multiplication of a few large intervals
588 auto const interval = 1'000'000'000;
589 auto const total = 10;
590 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
591
592 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
593 env.close();
594 }
595 {
596 // Overflow with multiplication of many small payments
597 // min interval is 60
598 auto const interval = 60;
599 auto const total = 1'000'000'000;
600 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
601
602 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
603 env.close();
604 }
605 {
606 // Overflow with an absurdly large grace period
607 // min interval is 60
608 auto const total = 60;
609 auto const interval = (maxLoanTime() - total) / total;
610 auto const grace = interval;
611 auto createJson = env.json(
612 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
613
614 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
615 env.close();
616 }
617 {
618 // Start date when the ledger is closed will be larger
619 auto const keylet = nextLoanKeylet(env, broker);
620
621 auto const grace = 100;
622 auto const interval = maxLoanTime() - grace;
623 auto const total = 1;
624 auto createJson = env.json(
625 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
626
627 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
628 env.close();
629
630 // The transaction is killed in the closed ledger
631 auto const meta = env.meta();
632 if (BEAST_EXPECT(meta))
633 {
634 BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED);
635 }
636
637 // If the transaction had succeeded, the loan would exist
638 auto const loanSle = env.le(keylet);
639 // but it doesn't
640 BEAST_EXPECT(!loanSle);
641 }
642 {
643 // Start date when the ledger is closed will be larger
644 auto const keylet = nextLoanKeylet(env, broker);
645
646 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
647 auto const grace = 5'000;
648 auto const interval = kMaxTime - closeStartDate - grace;
649 auto const total = 1;
650 auto createJson = env.json(
651 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
652
653 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
654 env.close();
655
656 // The transaction succeeds in the closed ledger
657 auto const meta = env.meta();
658 if (BEAST_EXPECT(meta))
659 {
660 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
661 }
662
663 // This loan exists
664 auto const afterState = getCurrentState(env, broker, keylet);
665 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
666 BEAST_EXPECT(afterState.previousPaymentDate == 0);
667 BEAST_EXPECT(afterState.paymentRemaining == 1);
668 }
669
670 {
671 // Ensure the borrower has funds to pay back the loan
672 env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2})));
673
674 // Start date when the ledger is closed will be larger
675 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
676 auto const grace = 5'000;
677 auto const maxLoanTime = kMaxTime - closeStartDate - grace;
678 auto const total = [&]() {
679 if (maxLoanTime % 5 == 0)
680 return 5;
681 if (maxLoanTime % 3 == 0)
682 return 3;
683 if (maxLoanTime % 2 == 0)
684 return 2;
685 return 0;
686 }();
687 if (!BEAST_EXPECT(total != 0))
688 return;
689
690 auto const brokerState = env.le(keylet::loanBroker(broker.brokerID));
691 if (!BEAST_EXPECT(brokerState))
692 return;
693 // Intentionally shadow the outer values
694 auto const loanSequence = brokerState->at(sfLoanSequence);
695 auto const keylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
696
697 auto const interval = maxLoanTime / total;
698 auto createJson = env.json(
699 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
700
701 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
702 env.close();
703
704 // This loan exists
705 auto const beforeState = getCurrentState(env, broker, keylet);
706 BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval);
707 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
708 BEAST_EXPECT(beforeState.paymentRemaining == total);
709 BEAST_EXPECT(beforeState.periodicPayment > 0);
710
711 // pay all but the last payment
712 {
714 Number const payment = beforeState.periodicPayment * (total - 1);
715 XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)};
716 STAmount const paymentAmount =
717 roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale);
718 auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee));
719 env(loanPayTx, Ter(tesSUCCESS));
720 env.close();
721 }
722
723 // The loan is on the last payment
724 auto const afterState = getCurrentState(env, broker, keylet);
725 BEAST_EXPECT(afterState.paymentRemaining == 1);
726 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
727 BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval);
728 }
729 }
730
731 void
736
737 // Tests run under each entry in amendmentCombinations().
738 void
740 {
741#if LOAN_TODO
742 testLoanPayLateFullPaymentBypassesPenalties(features);
743#endif
745 testDosLoanPay(features);
747 }
748
749public:
750 void
751 run() override
752 {
754 for (auto const& features : jtx::amendmentCombinations(
755 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
756 runAmendmentSensitive(features);
757 }
758};
759
761
762} // namespace xrpl::test
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Value removeMember(char const *key)
Remove and return the named member.
static constexpr std::uint32_t kDefaultGracePeriod
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
void run() override
Runs the suite.
void testDosLoanPay(FeatureBitset features)
void testLoanSetNearZeroInterestRateSucceeds()
void runAmendmentSensitive(FeatureBitset features)
void testLoanNextPaymentDueDateOverflow(FeatureBitset features)
void testOverpaymentManagementFee(FeatureBitset features)
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)
FeatureBitset const all_
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const &params=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)
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.
Definition jtx/Account.h:21
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
TER ter() const
Return the TER for the last JTx.
Definition Env.h:842
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:750
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:538
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
Set the fee on a JTx.
Definition fee.h:20
Inject raw JSON.
Definition jtx_json.h:16
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
T is_same_v
T max(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
@ Object
object value (collection of name/value pairs).
Definition json_value.h:29
Keylet computation functions.
Definition Indexes.h:40
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:567
json::Value set(AccountID const &account, uint256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
auto const kOverpaymentInterestRate
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
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.
Definition Env.h:123
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telINSUF_FEE_P
Definition TER.h:43
bool set(T &target, std::string const &name, Section const &section)
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.
Definition Issue.h:108
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:126
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
static FunctionType fixed(Keylet const &keylet)
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
boost::outcome_v2::result< T, std::error_code > Result
Definition b58_utils.h:19
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
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.
Definition STAmount.h:735
@ temBAD_AMOUNT
Definition TER.h:77
bool isTesSuccess(TER x) noexcept
Definition TER.h:676
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecPRECISION_LOSS
Definition TER.h:366
@ tecKILLED
Definition TER.h:319
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
@ tesSUCCESS
Definition TER.h:245
Number computeFullPaymentInterest(Number const &theoreticalPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)