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/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>
13
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>
35
36#include <algorithm>
37#include <cstdint>
38#include <limits>
39#include <memory>
40#include <string>
41#include <type_traits>
42
43namespace xrpl::test {
44
46{
47private:
48#if LOAN_TODO
49 void
50 testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features)
51 {
52 testcase("LoanPay full payment skips late penalties");
53 using namespace jtx;
54 using namespace loan;
55 using namespace std::chrono_literals;
56
57 Env env(*this, features);
58
59 Account const issuer{"issuer"};
60 Account const lender{"lender"};
61 Account const borrower{"borrower"};
62
63 env.fund(XRP(1'000'000), issuer, lender, borrower);
64 env.close();
65
66 PrettyAsset const asset = issuer[iouCurrency];
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)));
71 env.close();
72
73 BrokerInfo broker{createVaultAndBroker(env, asset, lender)};
74
75 auto const loanSetFee = Fee(env.current()->fees().base * 2);
76
77 auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID));
78 if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
79 return;
80
81 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
82 auto const loanKeylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
83
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();
88
89 env(set(borrower, broker.brokerID, principal),
90 Sig(sfCounterpartySignature, lender),
91 kLoanServiceFee(serviceFee),
92 kLatePaymentFee(lateFee),
93 kClosePaymentFee(closeFee),
94 kInterestRate(percentageToTenthBips(12)),
95 kLateInterestRate(percentageToTenthBips(24) / 10),
96 kCloseInterestRate(percentageToTenthBips(5)),
97 kPaymentTotal(12),
98 kPaymentInterval(600),
99 kGracePeriod(0),
100 Fee(loanSetFee));
101 env.close();
102
103 auto state1 = getCurrentState(env, broker, loanKeylet);
104 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
105 return;
106
107 using D = NetClock::duration;
108 using Tp = NetClock::time_point;
109 auto const overdueClose = Tp{D{state1.nextPaymentDate + state1.paymentInterval}};
110 env.close(overdueClose);
111
112 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
113 auto const loanSle = env.le(loanKeylet);
114 if (!BEAST_EXPECT(brokerSle && loanSle))
115 return;
116
117 auto state = getCurrentState(env, broker, loanKeylet);
118
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)};
123
124 Number const closePaymentFeeRounded =
125 roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale);
126 Number const latePaymentFeeRounded =
127 roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale);
128
129 auto const roundedLoanState = constructLoanState(
130 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
131 Number const totalInterestOutstanding = roundedLoanState.interestDue;
132
133 auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval);
134 auto const rawLoanState = computeTheoreticalLoanState(
135 env.current()->rules(),
136 state.periodicPayment,
137 periodicRate,
138 state.paymentRemaining,
139 managementFeeRate);
140
141 auto const parentCloseTime = env.current()->parentCloseTime();
142 auto const startDateSeconds =
143 static_cast<std::uint32_t>(state.startDate.time_since_epoch().count());
144
145 Number const fullPaymentInterest = computeFullPaymentInterest(
146 rawLoanState.principalOutstanding,
147 periodicRate,
148 parentCloseTime,
149 state.paymentInterval,
150 state.previousPaymentDate,
151 startDateSeconds,
152 closeInterestRateValue);
153
154 Number const roundedFullInterestAmount =
155 roundToAsset(broker.asset, fullPaymentInterest, state.loanScale);
156 Number const roundedFullManagementFee = computeManagementFee(
157 broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale);
158 Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee;
159
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;
165
166 Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee;
167 BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale));
168
169 auto const overdueSeconds =
170 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
171 if (!BEAST_EXPECT(overdueSeconds > 0))
172 return;
173
174 Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds);
175 Number const lateInterestRaw = state.principalOutstanding * overdueRate;
176 Number const lateInterestRounded =
177 roundToAsset(broker.asset, lateInterestRaw, state.loanScale);
178 Number const lateManagementFeeRounded = computeManagementFee(
179 broker.asset, lateInterestRounded, managementFeeRate, state.loanScale);
180 Number const penaltyDue =
181 lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded;
182 BEAST_EXPECT(penaltyDue > Number{});
183
184 auto const balanceBefore = env.balance(borrower, broker.asset).number();
185
186 STAmount const paymentAmount{broker.asset.raw(), baseFullDue};
187 env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
188 env.close();
189
190 if (auto const meta = env.meta(); BEAST_EXPECT(meta))
191 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
192
193 auto const balanceAfter = env.balance(borrower, broker.asset).number();
194 Number const actualPaid = balanceBefore - balanceAfter;
195 BEAST_EXPECT(actualPaid == baseFullDue);
196
197 Number const expectedWithPenalty = baseFullDue + penaltyDue;
198 BEAST_EXPECT(expectedWithPenalty > actualPaid);
199 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
200 }
201#endif
202
203 void
205 {
206 testcase("testOverpaymentManagementFee");
207
208 using namespace jtx;
209 using namespace loan;
210
211 Env env{*this, features};
212
213 Account const lender{"lender"}, borrower{"borrower"};
214
215 env.fund(XRP(10'000'000), lender, borrower);
216 env.close();
217
218 PrettyAsset const asset{xrpIssue(), 1000};
219
220 auto const result = createVaultAndBroker(
221 env,
222 asset,
223 lender,
224 {
225 .vaultDeposit = asset(100'000).value(),
226 .managementFeeRate = TenthBips16(10'000),
227 });
228
229 auto const loanSetFee = Fee(env.current()->fees().base * 2);
230
231 auto const brokerSle = env.le(result.brokerKeylet());
232 if (!BEAST_EXPECT(brokerSle))
233 return;
234 auto const loanKeylet = keylet::loan(
235 result.brokerKeylet().key, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
236 env(loan::set(
237 borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment),
238 Sig(sfCounterpartySignature, lender),
239 loan::kPaymentInterval(86400 * 30),
242 loanSetFee);
243
244 // From calculator
245 auto const expectedOverpaymentManagementFee = Number{33333, 0};
246 auto const loanBrokerBalanceBefore = env.balance(lender);
247
248 auto const loanPayFee = Fee(env.current()->fees().base * 2);
249 env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee);
250 env.close();
251
252 BEAST_EXPECTS(
253 env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee,
254 "overpayment management fee mismatch; expected:" +
255 to_string(expectedOverpaymentManagementFee) +
256 " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore));
257 }
258
259 void
261 {
262 bool const feeCapped = features[fixCleanup3_1_3];
263
264 // From FIND-005
265 testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped");
266
267 using namespace jtx;
268 using namespace std::chrono_literals;
269 using namespace lending;
270 Env env(*this, features);
271
272 Account const issuer{"issuer"};
273 Account const lender{"lender"};
274 Account const borrower{"borrower"};
275
276 env.fund(XRP(1'000'000), issuer, lender, borrower);
277 env.close();
278
279 BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3));
280
281 PrettyAsset const iouAsset = issuer[iouCurrency_];
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)));
286 env.close();
287
288 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
289
290 using namespace loan;
291
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;
295
296 auto const createJson = env.json(
297 set(borrower, broker.brokerID, principalRequest),
298 Fee(loanSetFee),
299 Json(sfCounterpartySignature, json::ValueType::Object),
300 kClosePaymentFee(0),
301 kGracePeriod(60),
302 kInterestRate(TenthBips32(20930)),
303 kLateInterestRate(TenthBips32(77049)),
304 kLatePaymentFee(0),
305 kLoanServiceFee(0),
306 kOverpaymentFee(TenthBips32(7)),
307 kOverpaymentInterestRate(TenthBips32(66653)),
308 kPaymentInterval(60),
309 kPaymentTotal(3239184));
310
311 // There are enough payments due on this loan that it only needs to be
312 // created once, and can be paid on multiple times. Just don't create a
313 // gazillion test cases.
314 auto const keylet = nextLoanKeylet(env, broker);
315
316 env(createJson, Sig(sfCounterpartySignature, lender));
317 env.close();
318
319 auto const roundedPayment = [&]() {
320 auto const stateBefore = getCurrentState(env, broker, keylet);
321 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
322 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
323
324 return roundToAsset(
325 iouAsset,
326 stateBefore.periodicPayment,
327 stateBefore.loanScale,
329 }();
330
331 auto test = [&](int const payFactor,
332 int const feeFactor,
333 TER const expectedTer = tesSUCCESS) {
334 auto const stateBefore = getCurrentState(env, broker, keylet);
335 BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184);
336 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
337
338 Number const amount = roundedPayment * payFactor;
339 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount}));
340 XRPAmount const payFee{baseFee * feeFactor};
341 env(loanPayTx, Ter(expectedTer), Fee(payFee));
342 env.close();
343 auto const expectedChange = isTesSuccess(expectedTer)
344 ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor)
345 : 0;
346
347 auto const stateAfter = getCurrentState(env, broker, keylet);
348 BEAST_EXPECT(
349 stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange);
350 };
351
352 static constexpr std::int64_t kMaxFeeIncrements =
353 kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
354
355 TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P;
356
357 // * Amount well above threshold -> capped fee
358 // The original test case - way over the limit - more fee is always ok
359 test(1819878, 363976);
360 // The capped fee is only sufficient if the amendment is enabled.
361 test(1819878, kMaxFeeIncrements, failWithoutFix);
362
363 // * Amount exactly at threshold -> capped fee
364 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements);
365 // More fee is always ok
366 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10);
367
368 // * Amount below threshold -> normal calculation
369 test(1, 1);
370 test(kLoanPaymentsPerFeeIncrement * 2, 2);
371 test(0, 0, temBAD_AMOUNT);
372 test(0, 1, temBAD_AMOUNT);
373 // Fee difference rounds evenly
374 test(
375 kLoanMaximumPaymentsPerTransaction - 10,
376 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1,
378 test(
379 kLoanMaximumPaymentsPerTransaction - 10,
380 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement));
381 // More fee is always ok
382 test(
383 kLoanMaximumPaymentsPerTransaction - 10,
384 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3);
385 // Fee rounds up
386 for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under)
387 {
388 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P);
389 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements);
390 }
391 // Only when you get one less fee increment can you pay less
392 test(
393 kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement,
394 kMaxFeeIncrements - 1);
395 // And again, more fee is always ok.
396 test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements);
397 }
398
399 // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero
400 // rate). At such a near-zero rate the closed-form payment factor
401 // (1 + r)^n - 1 cancels catastrophically.
402 //
403 // Without fixCleanup3_2_0 the resulting amortization is degenerate and the
404 // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the
405 // amendment, computePowerMinusOneHybrid uses a numerically-stable series
406 // expansion, so the loan is created and the scheduled payments
407 // (2 * periodicPayment) cover the principal — no economic underpayment
408 // (yield theft).
409 //
410 // The test runs the same LoanSet under both amendment settings and pins the
411 // exact outcome for each.
412 void
414 {
415 testcase("LoanSet near-zero interest rate covers principal");
416
417 using namespace jtx;
418 using namespace loan;
419
420 Number const principalRequested{1000};
421
422 struct Result
423 {
424 TER ter = tesSUCCESS;
425 bool created = false;
426 std::int32_t loanScale = 0;
427 Number principal;
428 Number totalValue;
429 Number managementFee;
430 Number periodicPayment;
431 };
432
433 auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result {
434 Env env(*this, features);
435
436 Account const issuer{"issuer"};
437 Account const lender{"vaultOwner"};
438 Account const borrower{"borrower"};
439
440 PrettyAsset const iouAsset = createFundedRippleIouAsset(env, issuer, lender, borrower);
441
442 auto const broker = createVaultAndBroker(
443 env,
444 iouAsset,
445 lender,
446 {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}});
447
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 =
452 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
453
454 env(set(borrower, broker.brokerID, principalRequested),
455 Sig(sfCounterpartySignature, lender),
456 kInterestRate(TenthBips32{1}),
457 kPaymentTotal(2),
458 kPaymentInterval(400),
459 Fee(env.current()->fees().base * 2),
460 Ter(expectedTer));
461 env.close();
462
463 Result r;
464 r.ter = env.ter();
465 if (auto const loanSle = env.le(loanKeylet))
466 {
467 r.created = true;
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);
473 }
474 return r;
475 };
476
477 Result const fixed = runScenario(all_, tesSUCCESS);
478 Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS);
479
480 // Without the amendment, the catastrophically-cancelling closed-form
481 // payment factor produces a degenerate amortization that fails
482 // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no
483 // loan is created.
484 BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS);
485 BEAST_EXPECT(!legacy.created);
486
487 // With the amendment the stable series expansion produces a valid loan
488 // at loanScale -10.
489 BEAST_EXPECT(fixed.ter == tesSUCCESS);
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}));
494 BEAST_EXPECT(fixed.managementFee == beast::kZero);
495
496 // Periodic payment from the numerically-stable series expansion, and the
497 // scheduled total (2 * periodicPayment) which exceeds the 1000 principal
498 // — no economic underpayment / yield theft.
499 BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16}));
500 BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15}));
501 BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested);
502 }
503
504 void
506 {
507 // For FIND-013
508 testcase << "Prevent nextPaymentDueDate overflow";
509
510 using namespace jtx;
511 using namespace std::chrono_literals;
512 using namespace lending;
513 Env env{*this, features};
514
515 Account const issuer{"issuer"};
516 Account const lender{"lender"};
517 Account const borrower{"borrower"};
518
519 PrettyAsset const iouAsset =
520 createFundedIouAsset(env, issuer, lender, borrower, 100'000'000, 10'000'000);
521
522 BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}};
523 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
524
525 using namespace loan;
526
527 auto const loanSetFee = Fee(env.current()->fees().base * 2);
528
529 using TimeType = decltype(sfNextPaymentDueDate)::type::value_type;
531 constexpr TimeType kMaxTime = std::numeric_limits<TimeType>::max();
532 static_assert(kMaxTime == 4'294'967'295);
533
534 auto const baseJson = [&]() {
535 auto createJson = env.json(
536 set(borrower, broker.brokerID, Number{55524'81, -2}),
537 Fee(loanSetFee),
538 kClosePaymentFee(0),
539 kGracePeriod(LoanSet::kDefaultGracePeriod),
540 kInterestRate(TenthBips32(12833)),
541 kLateInterestRate(TenthBips32(77048)),
542 kLatePaymentFee(0),
543 kLoanOriginationFee(218),
544 Json(sfCounterpartySignature, json::ValueType::Object));
545
546 createJson.removeMember(sfSequence.getJsonName());
547
548 return createJson;
549 }();
550
551 auto const baseFee = env.current()->fees().base;
552
553 auto parentCloseTime = [&]() {
554 return env.current()->parentCloseTime().time_since_epoch().count();
555 };
556 auto maxLoanTime = [&]() {
557 auto const startDate = parentCloseTime();
558
559 BEAST_EXPECT(startDate >= 50);
560
561 return kMaxTime - startDate;
562 };
563
564 {
565 // straight-up overflow: interval
566 auto const interval = maxLoanTime() + 1;
567 auto const total = 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: total
575 // min interval is 60
576 auto const interval = 60;
577 auto const total = maxLoanTime() + 1;
578 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
579
580 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
581 env.close();
582 }
583 {
584 // straight-up overflow: grace period
585 // min interval is 60
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));
591
592 // The grace period can't be larger than the interval.
593 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
594 env.close();
595 }
596 {
597 // Overflow with multiplication of a few large intervals
598 auto const interval = 1'000'000'000;
599 auto const total = 10;
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 multiplication of many small payments
607 // min interval is 60
608 auto const interval = 60;
609 auto const total = 1'000'000'000;
610 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
611
612 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
613 env.close();
614 }
615 {
616 // Overflow with an absurdly large grace period
617 // min interval is 60
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));
623
624 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
625 env.close();
626 }
627 {
628 // Start date when the ledger is closed will be larger
629 auto const keylet = nextLoanKeylet(env, broker);
630
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));
636
637 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
638 env.close();
639
640 // The transaction is killed in the closed ledger
641 auto const meta = env.meta();
642 if (BEAST_EXPECT(meta))
643 {
644 BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED);
645 }
646
647 // If the transaction had succeeded, the loan would exist
648 auto const loanSle = env.le(keylet);
649 // but it doesn't
650 BEAST_EXPECT(!loanSle);
651 }
652 {
653 // Start date when the ledger is closed will be larger
654 auto const keylet = nextLoanKeylet(env, broker);
655
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));
662
663 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
664 env.close();
665
666 // The transaction succeeds in the closed ledger
667 auto const meta = env.meta();
668 if (BEAST_EXPECT(meta))
669 {
670 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
671 }
672
673 // This loan exists
674 auto const afterState = getCurrentState(env, broker, keylet);
675 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
676 BEAST_EXPECT(afterState.previousPaymentDate == 0);
677 BEAST_EXPECT(afterState.paymentRemaining == 1);
678 }
679
680 {
681 // Ensure the borrower has funds to pay back the loan
682 env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2})));
683
684 // Start date when the ledger is closed will be larger
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)
690 return 5;
691 if (maxLoanTime % 3 == 0)
692 return 3;
693 if (maxLoanTime % 2 == 0)
694 return 2;
695 return 0;
696 }();
697 if (!BEAST_EXPECT(total != 0))
698 return;
699
700 auto const brokerState = env.le(keylet::loanBroker(broker.brokerID));
701 if (!BEAST_EXPECT(brokerState))
702 return;
703 // Intentionally shadow the outer values
704 auto const loanSequence = brokerState->at(sfLoanSequence);
705 auto const keylet = keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
706
707 auto const interval = maxLoanTime / total;
708 auto createJson = env.json(
709 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
710
711 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
712 env.close();
713
714 // This loan exists
715 auto const beforeState = getCurrentState(env, broker, keylet);
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);
720
721 // pay all but the last payment
722 {
724 Number const payment = beforeState.periodicPayment * (total - 1);
725 XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)};
726 STAmount const paymentAmount =
727 roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale);
728 auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee));
729 env(loanPayTx, Ter(tesSUCCESS));
730 env.close();
731 }
732
733 // The loan is on the last payment
734 auto const afterState = getCurrentState(env, broker, keylet);
735 BEAST_EXPECT(afterState.paymentRemaining == 1);
736 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
737 BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval);
738 }
739 }
740
741 // Which pseudo-account is left holding an unauthorized trust line when the
742 // repayment lands.
743 enum class UnauthorizedPayee {
744 // The vault's own line, as VaultCreate leaves it.
746 // Same vault, but the issuer authorized the line by hand first.
748 // Vault line authorized, broker owner unable to take the fee, so the
749 // fee goes to the loan broker's pseudo-account instead.
751 };
752
753 // A vault holding an IOU whose issuer requires authorization ends up with
754 // its own trust line unauthorized: VaultCreate opens the line without the
755 // auth flag, and the pseudo-account has no key to sign a TrustSet for
756 // itself. Neither deposits nor loan origination look at that line, so the
757 // vault appears to work right up to the first repayment, which is the only
758 // step that has to credit the vault back.
759 //
760 // The loan broker's pseudo-account has the same defect for the same reason,
761 // and LoanPay reaches it whenever the broker owner cannot take the fee.
762 //
763 // The issuer can still repair either line by hand, because TrustSet accepts
764 // a line that already exists even when its owner is a pseudo-account.
765 void
767 {
768 using namespace jtx;
769
770 Account const issuer{"issuer"};
771 Account const lender{"lender"};
772 Account const borrower{"borrower"};
773
774 auto runTestCases = [&](FeatureBitset features, UnauthorizedPayee payee) {
775 bool const pseudoExempt = features[fixCleanup3_4_0];
776 // With the vault's line repaired by the issuer, the only remaining
777 // unauthorized payee is the broker's pseudo-account.
778 bool const expectSuccess = pseudoExempt || payee == UnauthorizedPayee::VaultAuthorized;
779
780 auto const payeeLabel = [payee]() -> char const* {
781 switch (payee)
782 {
784 return "vault";
786 return "vault authorized by the issuer";
788 return "loan broker";
789 }
790 return ""; // LCOV_EXCL_LINE
791 }();
792
793 testcase << "LoanPay crediting an unauthorized " << payeeLabel << ": pseudo-account "
794 << (pseudoExempt ? "exempt" : "not exempt");
795
796 Env env{*this, features};
797
798 env.fund(XRP(1'000'000), issuer, lender, borrower);
799 env.close();
800
801 env(fset(issuer, asfRequireAuth));
802 env.close();
803
804 PrettyAsset const asset = issuer[iouCurrency_];
805 env(trust(lender, asset(100'000'000)));
806 env(trust(borrower, asset(100'000'000)));
807 env.close();
808
809 // Authorize the two participants. Nothing asks the issuer to also
810 // authorize the vault, which is the whole point of this test.
811 env(trust(issuer, asset(0), lender, tfSetfAuth));
812 env(trust(issuer, asset(0), borrower, tfSetfAuth));
813 env.close();
814
815 env(pay(issuer, lender, asset(10'000'000)));
816 env(pay(issuer, borrower, asset(10'000)));
817 env.close();
818
819 // Creating the vault and funding it with deposits succeeds even
820 // though the vault cannot be authorized to hold the asset.
821 BrokerInfo const broker{createVaultAndBroker(env, asset, lender)};
822
823 auto const vaultSle = env.le(broker.vaultKeylet());
824 auto const brokerSle = env.le(broker.brokerKeylet());
825 if (!BEAST_EXPECT(vaultSle && brokerSle))
826 return;
827
828 Account const vaultPseudo{"vault pseudo-account", vaultSle->at(sfAccount)};
829 Account const brokerPseudo{"broker pseudo-account", brokerSle->at(sfAccount)};
830
831 auto const lineIsAuthorized = [&](Account const& holder) -> bool {
832 auto const line = env.le(keylet::trustLine(holder, asset.raw().get<Issue>()));
833 if (!BEAST_EXPECT(line))
834 return false;
835 return line->isFlag(holder.id() > issuer.id() ? lsfLowAuth : lsfHighAuth);
836 };
837
838 BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
839 BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
840
841 if (payee != UnauthorizedPayee::Vault)
842 {
843 env(trust(issuer, asset(0), vaultPseudo, tfSetfAuth));
844 env.close();
845 BEAST_EXPECT(lineIsAuthorized(vaultPseudo));
846 }
847
848 using namespace loan;
849
850 // The service fee guarantees the broker is owed something on the
851 // first payment, so the broker leg of the transfer is exercised.
852 Number const serviceFee = asset(2).value();
853 auto const loanKeylet = nextLoanKeylet(env, broker);
854 env(set(borrower, broker.brokerID, asset(1'000).value()),
855 Sig(sfCounterpartySignature, lender),
856 kLoanServiceFee(serviceFee),
857 kInterestRate(percentageToTenthBips(12)),
858 kPaymentTotal(12),
859 kPaymentInterval(600),
860 Fee(env.current()->fees().base * 2));
861 env.close();
862
863 // Paying the principal out of the vault never needed authorization.
864 BEAST_EXPECT(env.le(loanKeylet));
865
866 if (payee == UnauthorizedPayee::Broker)
867 {
868 // A deep-frozen owner cannot take the fee, so LoanPay pays it
869 // into the broker's pseudo-account instead.
870 env(trust(issuer, asset(0), lender, tfSetFreeze | tfSetDeepFreeze));
871 env.close();
872 }
873
874 auto const state = getCurrentState(env, broker, loanKeylet);
875 STAmount const payment{
876 broker.asset,
878 broker.asset, state.periodicPayment + serviceFee, state.loanScale)};
879
880 // Repayment turns an outstanding loan back into cash the vault can
881 // lend again, so AssetsAvailable is what moves. AssetsTotal already
882 // counted the loan.
883 auto const assetsAvailable = [&]() -> Number {
884 auto const sle = env.le(broker.vaultKeylet());
885 if (!BEAST_EXPECT(sle))
886 return Number{};
887 return sle->at(sfAssetsAvailable);
888 };
889
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();
894
895 env(pay(borrower, loanKeylet.key, payment),
896 Ter(expectSuccess ? TER{tesSUCCESS} : TER{tecNO_AUTH}));
897 env.close();
898
899 if (expectSuccess)
900 {
901 BEAST_EXPECT(env.balance(borrower, asset).number() < borrowerBefore);
902 BEAST_EXPECT(env.balance(vaultPseudo, asset).number() > vaultBefore);
903 BEAST_EXPECT(assetsAvailable() > assetsAvailableBefore);
904 // Confirms the broker variant really did route the fee to the
905 // pseudo-account rather than to the owner.
906 BEAST_EXPECT(
907 (env.balance(brokerPseudo, asset).number() > brokerBefore) ==
908 (payee == UnauthorizedPayee::Broker));
909
910 // The payee is skipped by the check, not authorized by it: the line that just
911 // took the credit is still missing its auth flag.
912 if (payee == UnauthorizedPayee::Vault)
913 BEAST_EXPECT(!lineIsAuthorized(vaultPseudo));
914 if (payee == UnauthorizedPayee::Broker)
915 BEAST_EXPECT(!lineIsAuthorized(brokerPseudo));
916 }
917 else
918 {
919 // A rejected repayment must leave every balance untouched.
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);
924 }
925 };
926
927 for (auto const& features : {all_, all_ - fixCleanup3_4_0})
928 {
929 runTestCases(features, UnauthorizedPayee::Vault);
930 runTestCases(features, UnauthorizedPayee::VaultAuthorized);
931 runTestCases(features, UnauthorizedPayee::Broker);
932 }
933 }
934
935 void
937 {
938 // Regression test: LoanPay::doApply's fund-conservation check used to
939 // read XRP balances via accountHolds(..., SpendableHandling::
940 // FullBalance), which for XRP always defers to xrpLiquid (balance
941 // minus reserve, clamped at zero). When the broker fee landed on a
942 // payee sitting below its own reserve, that payee's clamped balance
943 // stayed zero and the fee vanished from the conservation sum,
944 // tripping "funds are conserved (with rounding)".
945 testcase("LoanPay funds conserved: broker fee payee below reserve");
946
947 using namespace jtx;
948
949 Env env(*this, features);
950
951 Account const issuer{"issuer"};
952 Account const lender{"lender"};
953 Account const borrower{"borrower"};
954
955 // Broker defaults match the fuzz workload: ManagementFeeRate = 100
956 // tenth-bips. The service fee guarantees feePaid > 0 on the first
957 // regular payment.
958 BrokerParameters const brokerParams;
959 Number const serviceFeeValue{2};
960 LoanParameters const loanParams{
961 .account = borrower,
962 .counter = lender,
963 .principalRequest = 1000,
964 .serviceFee = serviceFeeValue,
965 .interest = TenthBips32{percentageToTenthBips(12)},
966 .payTotal = 12,
967 .payInterval = 3600};
968
969 auto const loanOpt =
970 createLoan(env, AssetType::XRP, brokerParams, loanParams, issuer, lender, borrower);
971 if (BEAST_EXPECT(loanOpt); !loanOpt.has_value())
972 return;
973 auto const& [broker, loanKeylet, brokerPseudo] = *loanOpt;
974
975 auto const vaultPseudo = [&]() {
976 auto const vaultSle = env.le(keylet::vault(broker.vaultID));
977 if (!BEAST_EXPECT(vaultSle))
978 return AccountID{};
979 return vaultSle->at(sfAccount);
980 }();
981
982 // Raw AccountRoot balance, matching LoanPay::doApply's conservation
983 // check (not the reserve-clamped accountHolds()/xrpLiquid() value).
984 auto rawBalance = [&](AccountID const& id) -> STAmount {
985 auto const sle = env.le(keylet::account(id));
986 if (!BEAST_EXPECT(sle))
987 return STAmount{};
988 return sle->getFieldAmount(sfBalance);
989 };
990 auto lenderReserve = [&] {
991 return env.current()->fees().accountReserve(ownerCount(env, lender), 1);
992 };
993
994 STAmount const baseFee{env.current()->fees().base};
995
996 // Park the lender (broker owner, fee payee) exactly at its reserve,
997 // then burn part of the reserve with an oversized transaction fee.
998 // Fees are exempt from the reserve check, so the balance ends up
999 // below the reserve.
1000 env(pay(lender, issuer, rawBalance(lender.id()) - lenderReserve() - baseFee));
1001 env(noop(lender), Fee(XRP(100)));
1002 env.close();
1003 BEAST_EXPECT(env.balance(lender) < lenderReserve());
1004
1005 // First regular payment, exactly the amount due.
1006 auto const state = getCurrentState(env, broker, loanKeylet);
1007 STAmount const serviceFee = broker.asset(serviceFeeValue);
1008 STAmount const roundedPeriodicPayment{
1009 broker.asset,
1010 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
1011 STAmount const totalDue = roundToScale(
1012 roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward);
1013
1014 auto const borrowerBefore = rawBalance(borrower.id());
1015 auto const vaultBefore = rawBalance(vaultPseudo);
1016 auto const lenderBefore = rawBalance(lender.id());
1017
1018 // Before the fix, this aborted inside LoanPay::doApply on
1019 // XRPL_ASSERT_PARTS(goodRounding, "xrpl::LoanPay::doApply", "funds
1020 // are conserved (with rounding)").
1021 env(loan::pay(borrower, loanKeylet.key, totalDue));
1022 env.close();
1023
1024 auto const borrowerAfter = rawBalance(borrower.id());
1025 auto const vaultAfter = rawBalance(vaultPseudo);
1026 auto const lenderAfter = rawBalance(lender.id());
1027
1028 // The broker fee reached the lender's AccountRoot, even though the
1029 // lender's balance remains below its reserve.
1030 BEAST_EXPECT(lenderAfter > lenderBefore);
1031 BEAST_EXPECT(lenderAfter < lenderReserve());
1032
1033 // Total funds conserved across the payer, vault, and fee payee.
1034 BEAST_EXPECT(
1035 borrowerBefore - baseFee + vaultBefore + lenderBefore ==
1036 borrowerAfter + vaultAfter + lenderAfter);
1037 }
1038
1039 // Env::close() cannot land the ledger's parentCloseTime on an arbitrary
1040 // instant: it always rounds the requested time forward to the next
1041 // close-time-resolution boundary (see Env::close() and
1042 // roundCloseTime()/effCloseTime() in LedgerTiming.h), so it can only be
1043 // used to reach times strictly *after* a given due date, never exactly
1044 // on it. To pin the exact-boundary behavior of isPaymentLate(), directly
1045 // overwrite the loan's NextPaymentDueDate so that it matches the
1046 // *current* (already fixed) parentCloseTime of the open ledger, without
1047 // closing again. This exercises the same comparison
1048 // (parentCloseTime vs. NextPaymentDueDate) at the exact boundary that
1049 // env.close() cannot reliably reach.
1050 void
1051 setLoanNextPaymentDueDate(jtx::Env& env, Keylet const& loanKeylet, std::uint32_t dueDate)
1052 {
1053 using namespace jtx;
1054 bool const ok = env.app().getOpenLedger().modify([&](OpenView& view, beast::Journal) {
1055 auto const sle = view.read(loanKeylet);
1056 if (!sle)
1057 return false;
1058 auto replacement = std::make_shared<SLE>(*sle);
1059 (*replacement)[sfNextPaymentDueDate] = dueDate;
1060 view.rawReplace(replacement);
1061 return true;
1062 });
1063 BEAST_EXPECT(ok);
1064 }
1065
1066 // With fixCleanup3_4_0, isPaymentLate() uses a strict (Exclusive)
1067 // comparison: a payment due exactly "now" is not yet late. A plain
1068 // (non-late) LoanPay submitted at the exact NextPaymentDueDate instant
1069 // must therefore succeed, advance the due date by exactly one
1070 // PaymentInterval, and charge only the regular periodic payment amount
1071 // (no late interest / late fee).
1072 void
1074 {
1075 testcase("LoanPay at exact due date succeeds with fixCleanup3_4_0");
1076
1077 using namespace jtx;
1078 using namespace loan;
1079
1080 Env env(*this, all_);
1081 BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
1082
1083 Account const lender{"lender"};
1084 Account const borrower{"borrower"};
1085
1086 env.fund(XRP(10'000'000), lender, borrower);
1087 env.close();
1088
1089 PrettyAsset const asset{xrpIssue(), 1000};
1090 auto const broker = createVaultAndBroker(env, asset, lender);
1091
1092 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1093 if (!BEAST_EXPECT(brokerSle))
1094 return;
1095 auto const loanKeylet =
1096 keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1097
1098 // Set a large, non-zero late interest rate and late fee so that if
1099 // the late-payment path were incorrectly taken, the extra charge
1100 // would be large and easy to detect (far more than any rounding
1101 // slack in the regular periodic payment amount).
1102 env(set(borrower, broker.brokerID, asset(1'000).value()),
1103 Sig(sfCounterpartySignature, lender),
1104 kPaymentTotal(12),
1105 kPaymentInterval(600),
1106 kLateInterestRate(TenthBips32(percentageToTenthBips(24))),
1107 kLatePaymentFee(asset(50).value()),
1108 Fee(env.current()->fees().base * 2));
1109 env.close();
1110
1111 auto const stateBefore = getCurrentState(env, broker, loanKeylet);
1112 BEAST_EXPECT(stateBefore.paymentRemaining == 12);
1113
1114 STAmount const roundedPeriodicPayment{
1115 asset, roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale)};
1116
1117 // Set NextPaymentDueDate to exactly the current parentCloseTime,
1118 // without closing the ledger again.
1119 std::uint32_t const exactDueDate =
1120 env.current()->parentCloseTime().time_since_epoch().count();
1121 setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
1122
1123 STAmount const payFee{env.current()->fees().base};
1124 auto const borrowerBefore = env.balance(borrower, asset).number();
1125
1126 // A plain payment (no tfLoanLatePayment) for exactly the regular
1127 // periodic amount must succeed: at this instant the payment is not
1128 // yet late.
1129 //
1130 // Note: deliberately not calling env.close() after this: closing
1131 // the ledger re-derives the resulting state from the last validated
1132 // ledger plus the recorded transaction set, which would discard the
1133 // direct NextPaymentDueDate override made above via rawReplace().
1134 // Reading state from the still-open ledger (as env.le()/env.balance()
1135 // do) reflects the transaction as it was actually applied.
1136 env(pay(borrower, loanKeylet.key, roundedPeriodicPayment), Fee(payFee), Ter(tesSUCCESS));
1137
1138 auto const borrowerAfter = env.balance(borrower, asset).number();
1139
1140 // No more than the regular periodic amount (plus the transaction
1141 // fee) was charged: if the late-payment path had wrongly been
1142 // taken, the (large, non-zero) late interest and late fee set above
1143 // would have pushed the charge well past this bound.
1144 Number const charged = borrowerBefore - borrowerAfter - Number{payFee};
1145 BEAST_EXPECT(charged > Number{});
1146 BEAST_EXPECT(charged <= Number{roundedPeriodicPayment});
1147
1148 auto const stateAfter = getCurrentState(env, broker, loanKeylet);
1149 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining - 1);
1150 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate + stateBefore.paymentInterval);
1151 }
1152
1153 // Pins the amendment gate itself (as opposed to
1154 // testLoanPayAtExactDueDateSucceedsPostAmendment, which pins the
1155 // comparison operator): without fixCleanup3_4_0, isPaymentLate() keeps
1156 // using the pre-amendment Inclusive comparison, so a payment due exactly
1157 // "now" is already considered late, and a plain (non-late) LoanPay is
1158 // rejected.
1159 void
1161 {
1162 testcase("LoanPay at exact due date fails without fixCleanup3_4_0");
1163
1164 using namespace jtx;
1165 using namespace loan;
1166
1167 Env env(*this, all_ - fixCleanup3_4_0);
1168 BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
1169
1170 Account const lender{"lender"};
1171 Account const borrower{"borrower"};
1172
1173 env.fund(XRP(10'000'000), lender, borrower);
1174 env.close();
1175
1176 PrettyAsset const asset{xrpIssue(), 1000};
1177 auto const broker = createVaultAndBroker(env, asset, lender);
1178
1179 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1180 if (!BEAST_EXPECT(brokerSle))
1181 return;
1182 auto const loanKeylet =
1183 keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1184
1185 env(set(borrower, broker.brokerID, asset(1'000).value()),
1186 Sig(sfCounterpartySignature, lender),
1187 kPaymentTotal(12),
1188 kPaymentInterval(600),
1189 Fee(env.current()->fees().base * 2));
1190 env.close();
1191
1192 auto const stateBefore = getCurrentState(env, broker, loanKeylet);
1193 BEAST_EXPECT(stateBefore.paymentRemaining == 12);
1194
1195 STAmount const roundedPeriodicPayment{
1196 asset, roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale)};
1197
1198 // Set NextPaymentDueDate to exactly the current parentCloseTime,
1199 // without closing the ledger again.
1200 std::uint32_t const exactDueDate =
1201 env.current()->parentCloseTime().time_since_epoch().count();
1202 setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
1203
1204 // Without the amendment, the due date is already considered late at
1205 // this exact instant, so a plain payment must be rejected.
1206 //
1207 // Note: deliberately not calling env.close() after this: closing
1208 // the ledger re-derives the resulting state from the last validated
1209 // ledger plus the recorded transaction set, which would discard the
1210 // direct NextPaymentDueDate override made above via rawReplace().
1211 // Reading state from the still-open ledger (as env.le() does)
1212 // reflects the transaction as it was actually applied.
1213 env(pay(borrower, loanKeylet.key, roundedPeriodicPayment), Ter(tecEXPIRED));
1214
1215 auto const stateAfter = getCurrentState(env, broker, loanKeylet);
1216 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
1217 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
1218 }
1219
1220 // computeLatePayment() must agree with isPaymentLate() at the exact
1221 // due-date boundary: once fixCleanup3_4_0 is enabled, a payment due
1222 // exactly "now" is not yet late, so a tfLoanLatePayment submitted at
1223 // that same instant must be rejected with tecTOO_SOON rather than being
1224 // admitted and charged the late interest/fee.
1225 void
1227 {
1228 testcase("LoanPay(tfLoanLatePayment) at exact due date rejected with fixCleanup3_4_0");
1229
1230 using namespace jtx;
1231 using namespace loan;
1232
1233 Env env(*this, all_);
1234 BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
1235
1236 Account const lender{"lender"};
1237 Account const borrower{"borrower"};
1238
1239 env.fund(XRP(10'000'000), lender, borrower);
1240 env.close();
1241
1242 PrettyAsset const asset{xrpIssue(), 1000};
1243 auto const broker = createVaultAndBroker(env, asset, lender);
1244
1245 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1246 if (!BEAST_EXPECT(brokerSle))
1247 return;
1248 auto const loanKeylet =
1249 keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1250
1251 env(set(borrower, broker.brokerID, asset(1'000).value()),
1252 Sig(sfCounterpartySignature, lender),
1253 kPaymentTotal(12),
1254 kPaymentInterval(600),
1255 kLateInterestRate(TenthBips32(percentageToTenthBips(24))),
1256 kLatePaymentFee(asset(50).value()),
1257 Fee(env.current()->fees().base * 2));
1258 env.close();
1259
1260 auto const stateBefore = getCurrentState(env, broker, loanKeylet);
1261 BEAST_EXPECT(stateBefore.paymentRemaining == 12);
1262
1263 // Overpay generously so that, if the late-payment path were
1264 // incorrectly admitted, funds would not be the limiting factor;
1265 // we want to isolate the timing check itself.
1266 STAmount const generousAmount{
1267 asset,
1268 roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale) * 2};
1269
1270 // Set NextPaymentDueDate to exactly the current parentCloseTime,
1271 // without closing the ledger again.
1272 std::uint32_t const exactDueDate =
1273 env.current()->parentCloseTime().time_since_epoch().count();
1274 setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
1275
1276 // At this exact instant the loan is not yet late (Exclusive
1277 // comparison), so even an explicit late payment must be rejected
1278 // as premature, matching the plain-payment path.
1279 //
1280 // Note: deliberately not calling env.close() after this, for the
1281 // same reason given in testLoanPayAtExactDueDateSucceedsPostAmendment
1282 // above: closing would discard the direct NextPaymentDueDate
1283 // override made via rawReplace().
1284 env(pay(borrower, loanKeylet.key, generousAmount, tfLoanLatePayment), Ter(tecTOO_SOON));
1285
1286 auto const stateAfter = getCurrentState(env, broker, loanKeylet);
1287 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
1288 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
1289 }
1290
1291 // calculateBaseFee must use isPaymentLate(), not a raw inclusive
1292 // hasExpired(): once fixCleanup3_4_0 is enabled, a plain catch-up at
1293 // exactly NextPaymentDueDate succeeds and can process many payments, so
1294 // the fee has to scale with that work. Charging a single base fee here
1295 // would disagree with apply (and with the fixCleanup3_1_3 cap).
1296 void
1298 {
1299 testcase("LoanPay catch-up fee at exact due date with fixCleanup3_4_0");
1300
1301 using namespace jtx;
1302 using namespace loan;
1303 using namespace lending;
1304
1305 Env env(*this, all_);
1306 BEAST_EXPECT(env.enabled(fixCleanup3_4_0));
1307
1308 Account const lender{"lender"};
1309 Account const borrower{"borrower"};
1310
1311 env.fund(XRP(10'000'000), lender, borrower);
1312 env.close();
1313
1314 PrettyAsset const asset{xrpIssue(), 1000};
1315 auto const broker = createVaultAndBroker(env, asset, lender);
1316
1317 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1318 if (!BEAST_EXPECT(brokerSle))
1319 return;
1320 auto const loanKeylet =
1321 keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1322
1323 env(set(borrower, broker.brokerID, asset(10'000).value()),
1324 Sig(sfCounterpartySignature, lender),
1325 kPaymentTotal(50),
1326 kPaymentInterval(600),
1327 Fee(env.current()->fees().base * 2));
1328 env.close();
1329
1330 auto const stateBefore = getCurrentState(env, broker, loanKeylet);
1331 BEAST_EXPECT(stateBefore.paymentRemaining == 50);
1332 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanPaymentsPerFeeIncrement);
1333
1334 auto const loanSle = env.le(loanKeylet);
1335 if (!BEAST_EXPECT(loanSle))
1336 return;
1337 Number const regularPayment =
1338 roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale) +
1339 loanSle->at(sfLoanServiceFee);
1340 int const payCount = kLoanPaymentsPerFeeIncrement * 4;
1341 STAmount const catchUp{asset, regularPayment * payCount};
1342 XRPAmount const baseFee = env.current()->fees().base;
1343 XRPAmount const escalatedFee{baseFee * (payCount / kLoanPaymentsPerFeeIncrement)};
1344
1345 std::uint32_t const exactDueDate =
1346 env.current()->parentCloseTime().time_since_epoch().count();
1347 setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
1348
1349 // Under-fee: apply would process `payCount` payments, so a single
1350 // base fee is not enough.
1351 env(pay(borrower, loanKeylet.key, catchUp), Fee(baseFee), Ter(telINSUF_FEE_P));
1352
1353 // Same catch-up with the scaled fee must succeed at this instant.
1354 // Do not env.close() after the SLE override (see
1355 // testLoanPayAtExactDueDateSucceedsPostAmendment).
1356 env(pay(borrower, loanKeylet.key, catchUp), Fee(escalatedFee), Ter(tesSUCCESS));
1357
1358 auto const stateAfter = getCurrentState(env, broker, loanKeylet);
1359 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining - payCount);
1360 }
1361
1362 // Without the amendment, inclusive hasExpired still treats the exact
1363 // due-date instant as late, so calculateBaseFee correctly charges a
1364 // single base fee and apply rejects a plain LoanPay with tecEXPIRED.
1365 void
1367 {
1368 testcase("LoanPay catch-up fee at exact due date without fixCleanup3_4_0");
1369
1370 using namespace jtx;
1371 using namespace loan;
1372 using namespace lending;
1373
1374 Env env(*this, all_ - fixCleanup3_4_0);
1375 BEAST_EXPECT(!env.enabled(fixCleanup3_4_0));
1376
1377 Account const lender{"lender"};
1378 Account const borrower{"borrower"};
1379
1380 env.fund(XRP(10'000'000), lender, borrower);
1381 env.close();
1382
1383 PrettyAsset const asset{xrpIssue(), 1000};
1384 auto const broker = createVaultAndBroker(env, asset, lender);
1385
1386 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1387 if (!BEAST_EXPECT(brokerSle))
1388 return;
1389 auto const loanKeylet =
1390 keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1391
1392 env(set(borrower, broker.brokerID, asset(10'000).value()),
1393 Sig(sfCounterpartySignature, lender),
1394 kPaymentTotal(50),
1395 kPaymentInterval(600),
1396 Fee(env.current()->fees().base * 2));
1397 env.close();
1398
1399 auto const stateBefore = getCurrentState(env, broker, loanKeylet);
1400 BEAST_EXPECT(stateBefore.paymentRemaining == 50);
1401
1402 auto const loanSle = env.le(loanKeylet);
1403 if (!BEAST_EXPECT(loanSle))
1404 return;
1405 Number const regularPayment =
1406 roundPeriodicPayment(asset, stateBefore.periodicPayment, stateBefore.loanScale) +
1407 loanSle->at(sfLoanServiceFee);
1408 int const payCount = kLoanPaymentsPerFeeIncrement * 4;
1409 STAmount const catchUp{asset, regularPayment * payCount};
1410 XRPAmount const baseFee = env.current()->fees().base;
1411
1412 std::uint32_t const exactDueDate =
1413 env.current()->parentCloseTime().time_since_epoch().count();
1414 setLoanNextPaymentDueDate(env, loanKeylet, exactDueDate);
1415
1416 env(pay(borrower, loanKeylet.key, catchUp), Fee(baseFee), Ter(tecEXPIRED));
1417
1418 auto const stateAfter = getCurrentState(env, broker, loanKeylet);
1419 BEAST_EXPECT(stateAfter.paymentRemaining == stateBefore.paymentRemaining);
1420 BEAST_EXPECT(stateAfter.nextPaymentDate == exactDueDate);
1421 }
1422
1423 // LoanPay does not call canAddHolding. addEmptyHolding recreates the
1424 // broker-owner holding when the borrower is also the broker owner. After
1425 // fixCleanup3_4_0 an existing line is a no-op even if DefaultRipple is
1426 // off; pre-fix that path dies with tecINTERNAL.
1427 void
1429 {
1430 using namespace jtx;
1431 using namespace loan;
1432
1433 auto run = [this](FeatureBitset features, TER expected) {
1434 testcase(
1436 "LoanPay broker-owner borrower existing line after "
1437 "issuer clears asfDefaultRipple (") +
1438 (features[fixCleanup3_4_0] ? "post" : "pre") + "-fixCleanup3_4_0)");
1439
1440 Env env(*this, features);
1441 Account const issuer{"issuer"};
1442 Account const alice{"alice"};
1443
1444 env.fund(XRP(10'000), issuer, alice);
1445 env.close();
1446 env(fset(issuer, asfDefaultRipple));
1447 env.close();
1448
1449 PrettyAsset const usd{issuer["USD"]};
1450 env(trust(alice, usd(10'000'000)));
1451 env.close();
1452 env(pay(issuer, alice, usd(2'000'000)));
1453 env.close();
1454
1455 auto const broker = createVaultAndBroker(env, usd, alice);
1456 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1457 if (!BEAST_EXPECT(brokerSle))
1458 return;
1459 auto const loanKeylet =
1460 keylet::loan(broker.brokerID, SeqProxy::rawSequence(brokerSle->at(sfLoanSequence)));
1461
1462 Number const serviceFee = usd(2).value();
1463 env(set(alice, broker.brokerID, usd(1'000).value()),
1464 Sig(sfCounterpartySignature, alice),
1465 kLoanServiceFee(serviceFee),
1466 Fee(env.current()->fees().base * 2));
1467 env.close();
1468
1469 env(fclear(issuer, asfDefaultRipple));
1470 env.close();
1471 BEAST_EXPECT(env.le(keylet::trustLine(alice.id(), usd.raw().get<Issue>())));
1472
1473 auto const state = getCurrentState(env, broker, loanKeylet);
1474 STAmount const payment{
1475 usd,
1476 roundPeriodicPayment(usd, state.periodicPayment + serviceFee, state.loanScale)};
1477
1478 env(pay(alice, loanKeylet.key, payment), Ter(expected));
1479 env.close();
1480 };
1481
1482 run(all_ - fixCleanup3_4_0, tecINTERNAL);
1484 }
1485
1486 void
1498
1499 // Tests run under each entry in amendmentCombinations().
1500 void
1502 {
1503#if LOAN_TODO
1504 testLoanPayLateFullPaymentBypassesPenalties(features);
1505#endif
1508 testDosLoanPay(features);
1510 }
1511
1512public:
1513 void
1514 run() override
1515 {
1517 for (auto const& features : jtx::amendmentCombinations(
1518 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
1519 runAmendmentSensitive(features);
1520 }
1521};
1522
1524
1525} // namespace xrpl::test
A generic endpoint for log messages.
Definition Journal.h:44
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
Value removeMember(char const *key)
Remove and return the named member.
constexpr TIss const & get() const
A currency issued by an account.
Definition Issue.h:18
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
bool modify(ModifyType const &f)
Modify the open ledger.
Writable ledger view that accumulates state and tx changes.
Definition OpenView.h:59
void rawReplace(SLE::Ref sle) override
Unconditionally replace a state item.
Definition OpenView.cpp:244
SLE::const_pointer read(Keylet const &k) const override
Return the state item associated with a key.
Definition OpenView.cpp:168
Asset const & asset() const
Definition STAmount.h:496
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
virtual OpenLedger & getOpenLedger()=0
void run() override
Runs the suite.
void testLoanPayAtExactDueDateSucceedsPostAmendment()
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)
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)
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.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
Application & app()
Definition Env.h:300
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:844
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:752
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
bool enabled(UInt256 feature) const
Definition Env.h:886
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 make_shared(T... args)
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 vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:591
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:597
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:220
Keylet loan(UInt256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:603
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:275
json::Value set(AccountID const &account, UInt256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
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.
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 noop(Account const &account)
The null transaction.
Definition noop.h:14
std::uint32_t ownerCount(Env const &env, Account const &account)
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:110
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
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:127
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:657
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
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ temBAD_AMOUNT
Definition TER.h:77
bool isTesSuccess(TER x) noexcept
Definition TER.h:683
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
Definition TER.h:654
@ tecNO_AUTH
Definition TER.h:308
@ tecINTERNAL
Definition TER.h:318
@ tecTOO_SOON
Definition TER.h:326
@ tecEXPIRED
Definition TER.h:322
@ tecPRECISION_LOSS
Definition TER.h:371
@ tecKILLED
Definition TER.h:324
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
@ tesSUCCESS
Definition TER.h:250
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.
Definition Keylet.h:20