rippled
Loading...
Searching...
No Matches
Loan_test.cpp
1#include <xrpl/beast/unit_test/suite.h>
2//
3#include <test/jtx.h>
4#include <test/jtx/mpt.h>
5
6#include <xrpl/beast/xor_shift_engine.h>
7#include <xrpl/protocol/SField.h>
8#include <xrpl/server/LoadFeeTrack.h>
9#include <xrpl/tx/transactors/lending/LendingHelpers.h>
10#include <xrpl/tx/transactors/lending/LoanSet.h>
11#include <xrpl/tx/transactors/system/Batch.h>
12
13#include <chrono>
14
15namespace xrpl {
16namespace test {
17
19{
20protected:
21 // Ensure that all the features needed for Lending Protocol are included,
22 // even if they are set to unsupported.
24 jtx::testable_amendments() | featureMPTokensV1 | featureSingleAssetVault |
25 featureLendingProtocol};
26
28
29 void
31 {
32 testcase("Disabled");
33 // Lending Protocol depends on Single Asset Vault (SAV). Test
34 // combinations of the two amendments.
35 // Single Asset Vault depends on MPTokensV1, but don't test every combo
36 // of that.
37 using namespace jtx;
38 auto failAll = [this](FeatureBitset features) {
39 Env env(*this, features);
40
41 Account const alice{"alice"};
42 Account const bob{"bob"};
43 env.fund(XRP(10000), alice, bob);
44
45 auto const keylet = keylet::loanbroker(alice, env.seq(alice));
46
47 using namespace std::chrono_literals;
48 using namespace loan;
49
50 // counter party signature is optional on LoanSet. Confirm that by
51 // sending transaction without one.
52 auto setTx = env.jt(set(alice, keylet.key, Number(10000)), ter(temDISABLED));
53 env(setTx);
54
55 // All loan transactions are disabled.
56 // 1. LoanSet
57 setTx = env.jt(setTx, sig(sfCounterpartySignature, bob), ter(temDISABLED));
58 env(setTx);
59 // Actual sequence will be based off the loan broker, but we
60 // obviously don't have one of those if the amendment is disabled
61 auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice));
62 // Other Loan transactions are disabled, too.
63 // 2. LoanDelete
64 env(del(alice, loanKeylet.key), ter(temDISABLED));
65 // 3. LoanManage
66 env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED));
67 // 4. LoanPay
68 env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED));
69 };
70 failAll(all - featureMPTokensV1);
71 failAll(all - featureSingleAssetVault - featureLendingProtocol);
72 failAll(all - featureSingleAssetVault);
73 failAll(all - featureLendingProtocol);
74 }
75
77 {
78 Number vaultDeposit = 1'000'000;
79 Number debtMax = 25'000;
81 int coverDeposit = 1000;
84 std::string data = {}; // NOLINT(readability-redundant-member-init)
86
87 Number
88 maxCoveredLoanValue(Number const& currentDebt) const
89 {
91 auto debtLimit = coverDeposit * tenthBipsPerUnity.value() / coverRateMin.value();
92
93 return debtLimit - currentDebt;
94 }
95
96 static BrokerParameters const&
98 {
99 static BrokerParameters const result{};
100 return result;
101 }
102
103 // TODO: create an operator() which returns a transaction similar to
104 // LoanParameters
105 };
106
108 {
114 jtx::PrettyAsset const& asset_,
115 Keylet const& brokerKeylet_,
116 Keylet const& vaultKeylet_,
117 BrokerParameters const& p)
118 : asset(asset_), brokerID(brokerKeylet_.key), vaultID(vaultKeylet_.key), params(p)
119 {
120 }
121
122 Keylet
124 {
126 }
127 Keylet
129 {
130 return keylet::vault(vaultID);
131 }
132
133 int
134 vaultScale(jtx::Env const& env) const
135 {
136 using namespace jtx;
137
138 auto const vaultSle = env.le(keylet::vault(vaultID));
139 return getAssetsTotalScale(vaultSle);
140 }
141 };
142
144 {
145 // The account submitting the transaction. May be borrower or broker.
147 // The counterparty. Should be the other of borrower or broker.
149 // Whether the counterparty is specified in the `counterparty` field, or
150 // only signs.
153 // NOLINTBEGIN(readability-redundant-member-init)
168 // NOLINTEND(readability-redundant-member-init)
169
170 template <class... FN>
172 operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const
173 {
174 using namespace jtx;
175 using namespace jtx::loan;
176
177 JTx jt{loan::set(
178 account,
179 broker.brokerID,
180 broker.asset(principalRequest).number(),
181 flags.value_or(0))};
182
183 sig(sfCounterpartySignature, counter)(env, jt);
184
185 fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt);
186
188 counterparty(counter)(env, jt);
189 if (originationFee)
190 loanOriginationFee(broker.asset(*originationFee).number())(env, jt);
191 if (serviceFee)
192 loanServiceFee(broker.asset(*serviceFee).number())(env, jt);
193 if (lateFee)
194 latePaymentFee(broker.asset(*lateFee).number())(env, jt);
195 if (closeFee)
196 closePaymentFee(broker.asset(*closeFee).number())(env, jt);
197 if (overFee)
198 overpaymentFee (*overFee)(env, jt);
199 if (interest)
200 interestRate (*interest)(env, jt);
201 if (lateInterest)
202 lateInterestRate (*lateInterest)(env, jt);
203 if (closeInterest)
204 closeInterestRate (*closeInterest)(env, jt);
206 overpaymentInterestRate (*overpaymentInterest)(env, jt);
207 if (payTotal)
208 paymentTotal (*payTotal)(env, jt);
209 if (payInterval)
210 paymentInterval (*payInterval)(env, jt);
211 if (gracePd)
212 gracePeriod (*gracePd)(env, jt);
213
214 return env.jt(jt, fN...);
215 }
216 };
217
219 {
223 bool showStepBalances = false;
224 bool validateBalances = true;
225
226 static PaymentParameters const&
228 {
229 static PaymentParameters const result{};
230 return result;
231 }
232 };
233
249
254 {
255 public:
256 jtx::Env const& env;
260
262 jtx::Env const& env_,
263 BrokerInfo const& broker_,
264 jtx::Account const& pseudo_,
265 Keylet const& keylet_)
266 : env(env_), broker(broker_), pseudoAccount(pseudo_), loanKeylet(keylet_)
267 {
268 }
269
272 void
274 Number const& principalOutstanding,
275 Number const& interestOwed,
276 TenthBips32 interestRate,
277 std::uint32_t paymentInterval,
278 std::uint32_t paymentsRemaining,
280 {
281 using namespace jtx;
282 if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
283 env.test.BEAST_EXPECT(brokerSle))
284 {
285 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
286 auto const brokerDebt = brokerSle->at(sfDebtTotal);
287 auto const expectedDebt = principalOutstanding + interestOwed;
288 env.test.BEAST_EXPECT(brokerDebt == expectedDebt);
289 env.test.BEAST_EXPECT(
291 brokerSle->at(sfCoverAvailable));
292 env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount);
293
294 if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
295 env.test.BEAST_EXPECT(vaultSle))
296 {
297 Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)};
298 env.test.BEAST_EXPECT(
299 vaultSle->at(sfAssetsAvailable) ==
300 env.balance(vaultPseudo, broker.asset).number());
301 if (ownerCount == 0)
302 {
303 // Allow some slop for rounding IOUs
304
305 // TODO: This needs to be an exact match once all the
306 // other rounding issues are worked out.
307 auto const total = vaultSle->at(sfAssetsTotal);
308 auto const available = vaultSle->at(sfAssetsAvailable);
309 env.test.BEAST_EXPECT(
310 total == available ||
311 (!broker.asset.integral() && available != 0 &&
312 ((total - available) / available < Number(1, -6))));
313 env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
314 }
315 }
316 }
317 }
318
319 void
321 std::int32_t loanScale,
322 jtx::Account const& account,
323 jtx::PrettyAmount const& balanceBefore,
324 STAmount const& expectedPayment,
325 jtx::PrettyAmount const& adjustment) const
326 {
327 auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent());
328
329 STAmount const balanceChangeAmount{
331 roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)};
332 {
333 auto const difference = roundToScale(
334 env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount),
335 borrowerScale);
337 roundToScale(difference, loanScale) >= beast::zero,
338 "Balance before: " + to_string(balanceBefore.value()) +
339 ", expected change: " + to_string(balanceChangeAmount) +
340 ", difference (balance after - expected): " + to_string(difference),
341 __FILE__,
342 __LINE__);
343 }
344 }
345
347 void
349 std::uint32_t previousPaymentDate,
350 std::uint32_t nextPaymentDate,
351 std::uint32_t paymentRemaining,
352 Number const& loanScale,
353 Number const& totalValue,
354 Number const& principalOutstanding,
355 Number const& managementFeeOutstanding,
356 Number const& periodicPayment,
357 std::uint32_t flags) const
358 {
359 using namespace jtx;
360 if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
361 {
362 env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate);
363 env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining);
364 env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate);
365 env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale);
366 env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue);
367 env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding);
368 env.test.BEAST_EXPECT(
369 loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding);
370 env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment);
371 env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
372
373 auto const ls = constructRoundedLoanState(loan);
374
375 auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
376 auto const paymentInterval = loan->at(sfPaymentInterval);
378 principalOutstanding,
379 ls.interestDue,
380 interestRate,
381 paymentInterval,
382 paymentRemaining,
383 1);
384
385 if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
386 env.test.BEAST_EXPECT(brokerSle))
387 {
388 if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
389 env.test.BEAST_EXPECT(vaultSle))
390 {
391 if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u))
392 {
393 env.test.BEAST_EXPECT(
394 vaultSle->at(sfLossUnrealized) ==
395 totalValue - managementFeeOutstanding);
396 }
397 else
398 {
399 env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
400 }
401 }
402 }
403 }
404 }
405
407 void
408 operator()(LoanState const& state) const
409 {
412 state.nextPaymentDate,
413 state.paymentRemaining,
414 state.loanScale,
415 state.totalValue,
418 state.periodicPayment,
419 state.flags);
420 };
421 };
422
423 BrokerInfo
425 jtx::Env& env,
426 jtx::PrettyAsset const& asset,
427 jtx::Account const& lender,
429 {
430 using namespace jtx;
431
432 Vault const vault{env};
433
434 auto const deposit = asset(params.vaultDeposit);
435 auto const debtMaximumValue = asset(params.debtMax).value();
436 auto const coverDepositValue = asset(params.coverDeposit).value();
437
438 auto const coverRateMinValue = params.coverRateMin;
439
440 auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
441 env(tx);
442 env.close();
443 BEAST_EXPECT(env.le(vaultKeylet));
444
445 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
446 env.close();
447 if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault))
448 {
449 BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value());
450 }
451
452 auto const keylet = keylet::loanbroker(lender.id(), env.seq(lender));
453
454 using namespace loanBroker;
455 env(set(lender, vaultKeylet.key, params.flags),
456 data(params.data),
457 managementFeeRate(params.managementFeeRate),
458 debtMaximum(debtMaximumValue),
459 coverRateMinimum(coverRateMinValue),
460 coverRateLiquidation(TenthBips32(params.coverRateLiquidation)));
461
462 if (coverDepositValue != beast::zero)
463 env(coverDeposit(lender, keylet.key, coverDepositValue));
464
465 env.close();
466
467 return {asset, keylet, vaultKeylet, params};
468 }
469
472 getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet)
473 {
474 using d = NetClock::duration;
475 using tp = NetClock::time_point;
476
477 // Lookup the current loan state
478 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
479 {
480 return LoanState{
481 .previousPaymentDate = loan->at(sfPreviousPaymentDueDate),
482 .startDate = tp{d{loan->at(sfStartDate)}},
483 .nextPaymentDate = loan->at(sfNextPaymentDueDate),
484 .paymentRemaining = loan->at(sfPaymentRemaining),
485 .loanScale = loan->at(sfLoanScale),
486 .totalValue = loan->at(sfTotalValueOutstanding),
487 .principalOutstanding = loan->at(sfPrincipalOutstanding),
488 .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding),
489 .periodicPayment = loan->at(sfPeriodicPayment),
490 .flags = loan->at(sfFlags),
491 .paymentInterval = loan->at(sfPaymentInterval),
492 .interestRate = TenthBips32{loan->at(sfInterestRate)},
493 };
494 }
495 return LoanState{};
496 }
497
502 jtx::Env const& env,
503 BrokerInfo const& broker,
504 Keylet const& loanKeylet,
505 VerifyLoanStatus const& verifyLoanStatus)
506 {
507 using namespace std::chrono_literals;
508 using d = NetClock::duration;
509 using tp = NetClock::time_point;
510
511 auto const state = getCurrentState(env, broker, loanKeylet);
512 BEAST_EXPECT(state.previousPaymentDate == 0);
513 BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s);
514 BEAST_EXPECT(state.paymentRemaining == 12);
515 BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value());
516 BEAST_EXPECT(
517 state.loanScale >=
518 (broker.asset.integral()
519 ? 0
520 : std::max(broker.vaultScale(env), state.principalOutstanding.exponent())));
521 BEAST_EXPECT(state.paymentInterval == 600);
522 {
524 BEAST_EXPECT(
525 state.totalValue ==
527 broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale));
528 }
529 BEAST_EXPECT(
530 state.managementFeeOutstanding ==
532 broker.asset,
533 state.totalValue - state.principalOutstanding,
535 state.loanScale));
536
537 verifyLoanStatus(state);
538
539 return state;
540 }
541
542 bool
543 canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state)
544 {
545 if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
546 BEAST_EXPECT(brokerSle))
547 {
548 if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
549 BEAST_EXPECT(vaultSle))
550 {
551 // log << vaultSle->getJson() << std::endl;
552 auto const assetsUnavailable =
553 vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable);
554 auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + state.totalValue -
556
557 if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable))
558 {
559 return false;
560 }
561 }
562 }
563 return true;
564 }
565
566 enum class AssetType { XRP = 0, IOU = 1, MPT = 2 };
567
568 // Specify the accounts as params to allow other accounts to be used
571 jtx::Env& env,
572 AssetType assetType,
573 BrokerParameters const& brokerParams,
574 jtx::Account const& issuer,
575 jtx::Account const& lender,
576 jtx::Account const& borrower)
577 {
578 using namespace jtx;
579
580 switch (assetType)
581 {
582 case AssetType::XRP:
583 // TODO: remove the factor, and set up loans in drops
584 return PrettyAsset{xrpIssue(), 1'000'000};
585
586 case AssetType::IOU: {
587 PrettyAsset const asset{issuer[iouCurrency]};
588
589 auto const limit =
590 asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit));
591 if (lender != issuer)
592 env(trust(lender, limit));
593 if (borrower != issuer)
594 env(trust(borrower, limit));
595
596 return asset;
597 }
598
599 case AssetType::MPT: {
600 // Enough to cover initial fees
601 if (!env.le(keylet::account(issuer)))
602 env.fund(env.current()->fees().accountReserve(10) * 10, issuer);
603 if (!env.le(keylet::account(lender)))
604 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender));
605 if (!env.le(keylet::account(borrower)))
606 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower));
607
608 MPTTester mptt{env, issuer, mptInitNoFund};
609 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
610 // Scale the MPT asset so interest is interesting
611 PrettyAsset const asset{mptt.issuanceID(), 10'000};
612 // Need to do the authorization here because mptt isn't
613 // accessible outside
614 if (lender != issuer)
615 mptt.authorize({.account = lender});
616 if (borrower != issuer)
617 mptt.authorize({.account = borrower});
618
619 env.close();
620
621 return asset;
622 }
623
624 default:
625 throw std::runtime_error("Unknown asset type");
626 }
627 }
628
629 void
631 jtx::Env& env,
632 BrokerParameters const& brokerParams,
633 LoanParameters const& loanParams,
634 AssetType assetType,
635 jtx::Account const& issuer,
636 jtx::Account const& lender,
637 jtx::Account const& borrower)
638 {
639 using namespace jtx;
640
641 auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower);
642 auto const principal = asset(loanParams.principalRequest).number();
643 auto const interest = loanParams.interest.value_or(TenthBips32{});
644 auto const interval = loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval);
645 auto const total = loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal);
646 auto const feeRate = brokerParams.managementFeeRate;
647 auto const props = computeLoanProperties(
648 asset,
649 principal,
650 interest,
651 interval,
652 total,
653 feeRate,
654 asset(brokerParams.vaultDeposit).number().exponent());
655 log << "Loan properties:\n"
656 << "\tPrincipal: " << principal << std::endl
657 << "\tInterest rate: " << interest << std::endl
658 << "\tPayment interval: " << interval << std::endl
659 << "\tManagement Fee Rate: " << feeRate << std::endl
660 << "\tTotal Payments: " << total << std::endl
661 << "\tPeriodic Payment: " << props.periodicPayment << std::endl
662 << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl
663 << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl
664 << "\tLoan Scale: " << props.loanScale << std::endl
665 << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl;
666
667 // checkGuards returns a TER, so success is 0
668 BEAST_EXPECT(!checkLoanGuards(
669 asset,
670 asset(loanParams.principalRequest).number(),
671 loanParams.interest.value_or(TenthBips32{}) != beast::zero,
673 props,
674 env.journal));
675 }
676
679 jtx::Env& env,
680 AssetType assetType,
681 BrokerParameters const& brokerParams,
682 LoanParameters const& loanParams,
683 jtx::Account const& issuer,
684 jtx::Account const& lender,
685 jtx::Account const& borrower)
686 {
687 using namespace jtx;
688
689 // Enough to cover initial fees
690 env.fund(env.current()->fees().accountReserve(10) * 10, issuer);
691 if (lender != issuer)
692 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender));
693 if (borrower != issuer && borrower != lender)
694 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower));
695
696 describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower);
697
698 // Make the asset
699 auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower);
700
701 env.close();
702 if (asset.native() || lender != issuer)
703 {
704 env(
705 pay((asset.native() ? env.master : issuer),
706 lender,
707 asset(brokerParams.vaultDeposit + brokerParams.coverDeposit)));
708 }
709 // Fund the borrower later once we know the total loan
710 // size
711
712 BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams);
713
714 auto const pseudoAcctOpt = [&]() -> std::optional<Account> {
715 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
716 if (!BEAST_EXPECT(brokerSle))
717 return std::nullopt;
718 auto const brokerPseudo = brokerSle->at(sfAccount);
719 return Account("Broker pseudo-account", brokerPseudo);
720 }();
721 if (!pseudoAcctOpt)
722 return std::nullopt;
723 Account const& pseudoAcct = *pseudoAcctOpt;
724
725 auto const loanKeyletOpt = [&]() -> std::optional<Keylet> {
726 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
727 if (!BEAST_EXPECT(brokerSle))
728 return std::nullopt;
729
730 // Broker has no loans
731 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
732
733 // The loan keylet is based on the LoanSequence of the
734 // _LOAN_BROKER_ object.
735 auto const loanSequence = brokerSle->at(sfLoanSequence);
736 return keylet::loan(broker.brokerID, loanSequence);
737 }();
738 if (!loanKeyletOpt)
739 return std::nullopt;
740 Keylet const& loanKeylet = *loanKeyletOpt;
741
742 env(loanParams(env, broker));
743
744 env.close();
745
746 return std::make_tuple(broker, loanKeylet, pseudoAcct);
747 }
748
749 static void
751 jtx::Env& env,
752 BrokerInfo const& broker,
753 jtx::Account const& issuer,
754 jtx::Account const& borrower,
755 LoanState const& state,
756 std::optional<Number> const& servFee)
757 {
758 using namespace jtx;
759
760 STAmount const serviceFee = broker.asset(servFee.value_or(0));
761
762 // Ensure the borrower has enough funds to make the payments
763 // (including tx fees, if necessary)
764 auto const borrowerBalance = env.balance(borrower, broker.asset);
765
766 auto const baseFee = env.current()->fees().base;
767
768 // Add extra for transaction fees and reserves, if appropriate, or a
769 // tiny amount for the extra paid in each transaction
770 auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) +
771 (broker.asset.native()
772 ? Number(
773 baseFee * state.paymentRemaining +
774 env.current()->fees().accountReserve(env.ownerCount(borrower)))
775 : broker.asset(15).number());
776
777 auto const shortage = totalNeeded - borrowerBalance.number();
778
779 if (shortage > beast::zero && (broker.asset.native() || issuer != borrower))
780 {
781 env(
782 pay((broker.asset.native() ? env.master : issuer),
783 borrower,
784 STAmount{broker.asset, shortage}));
785 }
786 }
787
788 void
790 jtx::Env& env,
791 BrokerInfo const& broker,
792 LoanParameters const& loanParams,
793 Keylet const& loanKeylet,
794 VerifyLoanStatus const& verifyLoanStatus,
795 jtx::Account const& issuer,
796 jtx::Account const& lender,
797 jtx::Account const& borrower,
798 PaymentParameters const& paymentParams = PaymentParameters::defaults())
799 {
800 // Make all the individual payments
801 using namespace jtx;
802 using namespace jtx::loan;
803 using namespace std::chrono_literals;
804 using d = NetClock::duration;
805
806 bool const showStepBalances = paymentParams.showStepBalances;
807
808 auto const currencyLabel = getCurrencyLabel(broker.asset);
809
810 auto const baseFee = env.current()->fees().base;
811
812 env.close();
813 auto state = getCurrentState(env, broker, loanKeylet);
814
815 verifyLoanStatus(state);
816
817 STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0));
818
819 topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee);
820
821 // Periodic payment amount will consist of
822 // 1. principal outstanding (1000)
823 // 2. interest interest rate (at 12%)
824 // 3. payment interval (600s)
825 // 4. loan service fee (2)
826 // Calculate these values without the helper functions
827 // to verify they're working correctly The numbers in
828 // the below BEAST_EXPECTs may not hold across assets.
829 auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval);
830 STAmount const roundedPeriodicPayment{
831 broker.asset,
832 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
833
834 if (!showStepBalances)
835 {
836 log << currencyLabel << " Payment components: "
837 << "Payments remaining, "
838 << "rawInterest, rawPrincipal, "
839 "rawMFee, "
840 << "trackedValueDelta, trackedPrincipalDelta, "
841 "trackedInterestDelta, trackedMgmtFeeDelta, special"
842 << std::endl;
843 }
844
845 // Include the service fee
846 STAmount const totalDue =
847 roundToScale(roundedPeriodicPayment + serviceFee, state.loanScale, Number::upward);
848
849 auto currentRoundedState = constructLoanState(
850 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
851 {
852 auto const raw = computeTheoreticalLoanState(
853 state.periodicPayment,
854 periodicRate,
855 state.paymentRemaining,
857
858 if (showStepBalances)
859 {
860 log << currencyLabel << " Starting loan balances: "
861 << "\n\tTotal value: " << currentRoundedState.valueOutstanding
862 << "\n\tPrincipal: " << currentRoundedState.principalOutstanding
863 << "\n\tInterest: " << currentRoundedState.interestDue
864 << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue
865 << "\n\tPayments remaining " << state.paymentRemaining << std::endl;
866 }
867 else
868 {
869 log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", "
870 << raw.interestDue << ", " << raw.principalOutstanding << ", "
871 << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", "
872 << currentRoundedState.principalOutstanding << ", "
873 << currentRoundedState.interestDue << ", "
874 << currentRoundedState.managementFeeDue << std::endl;
875 }
876 }
877
878 // Try to pay a little extra to show that it's _not_
879 // taken
880 auto const extraAmount = paymentParams.overpaymentExtra
881 ? broker.asset(*paymentParams.overpaymentExtra).value()
882 : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20});
883
884 STAmount const transactionAmount =
885 STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount;
886
887 auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number();
888 auto const initialState = state;
890 .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0};
891 Number totalInterestPaid = 0;
892 Number totalFeesPaid = 0;
893 std::size_t totalPaymentsMade = 0;
894
896 state.periodicPayment,
897 periodicRate,
898 state.paymentRemaining,
900
901 auto validateBorrowerBalance = [&]() {
902 if (borrower == issuer || !paymentParams.validateBalances)
903 return;
904 auto const totalSpent =
905 (totalPaid.trackedValueDelta + totalFeesPaid +
906 (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : numZero));
907 BEAST_EXPECT(
908 env.balance(borrower, broker.asset).number() ==
909 borrowerInitialBalance - totalSpent);
910 };
911
912 auto const defaultRound = broker.asset.integral() ? 3 : 0;
913 auto truncate = [defaultRound](Number const& n, std::optional<int> places = std::nullopt) {
914 auto const p = places.value_or(defaultRound);
915 if (p == 0)
916 return n;
917 auto const factor = Number{1, p};
918 return (n * factor).truncate() / factor;
919 };
920 while (state.paymentRemaining > 0)
921 {
922 validateBorrowerBalance();
923 // Compute the expected principal amount
924 auto const paymentComponents = detail::computePaymentComponents(
925 broker.asset.raw(),
926 state.loanScale,
927 state.totalValue,
928 state.principalOutstanding,
929 state.managementFeeOutstanding,
930 state.periodicPayment,
931 periodicRate,
932 state.paymentRemaining,
934
935 BEAST_EXPECT(
936 paymentComponents.trackedValueDelta <= roundedPeriodicPayment ||
937 (paymentComponents.specialCase == detail::PaymentSpecialCase::final &&
938 paymentComponents.trackedValueDelta >= roundedPeriodicPayment));
939 BEAST_EXPECT(
940 paymentComponents.trackedValueDelta ==
941 paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() +
942 paymentComponents.trackedManagementFeeDelta);
943
944 xrpl::LoanState const nextTrueState = computeTheoreticalLoanState(
945 state.periodicPayment,
946 periodicRate,
947 state.paymentRemaining - 1,
949 detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState;
950 BEAST_EXPECT(
951 deltas.total() == deltas.principal + deltas.interest + deltas.managementFee);
952 BEAST_EXPECT(
953 paymentComponents.specialCase == detail::PaymentSpecialCase::final ||
954 deltas.total() == state.periodicPayment ||
955 (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14);
956
957 if (!showStepBalances)
958 {
959 log << currencyLabel << " Payment components: " << state.paymentRemaining << ", "
960
961 << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee
962 << ", " << paymentComponents.trackedValueDelta << ", "
963 << paymentComponents.trackedPrincipalDelta << ", "
964 << paymentComponents.trackedInterestPart() << ", "
965 << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* {
966 if (paymentComponents.specialCase == detail::PaymentSpecialCase::final)
967 return "final";
968 if (paymentComponents.specialCase == detail::PaymentSpecialCase::extra)
969 return "extra";
970 return "none";
971 }() << std::endl;
972 }
973
974 auto const totalDueAmount =
975 STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee};
976
977 if (paymentParams.validateBalances)
978 {
979 // Due to the rounding algorithms to keep the interest and
980 // principal in sync with "true" values, the computed amount
981 // may be a little less than the rounded fixed payment
982 // amount. For integral types, the difference should be < 3
983 // (1 unit for each of the interest and management fee). For
984 // IOUs, the difference should be dust.
985 Number const diff = totalDue - totalDueAmount;
986 BEAST_EXPECT(
987 paymentComponents.specialCase == detail::PaymentSpecialCase::final ||
988 diff == beast::zero ||
989 (diff > beast::zero &&
990 ((broker.asset.integral() && (static_cast<Number>(diff) < 3)) ||
991 (state.loanScale - diff.exponent() > 13))));
992
993 BEAST_EXPECT(
994 paymentComponents.trackedPrincipalDelta >= beast::zero &&
995 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
996 BEAST_EXPECT(
997 paymentComponents.specialCase != detail::PaymentSpecialCase::final ||
998 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
999 }
1000
1001 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
1002
1003 // Make the payment
1004 env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags));
1005
1006 env.close(d{state.paymentInterval / 2});
1007
1008 if (paymentParams.validateBalances)
1009 {
1010 // Need to account for fees if the loan is in XRP
1011 PrettyAmount adjustment = broker.asset(0);
1012 if (broker.asset.native())
1013 {
1014 adjustment = env.current()->fees().base;
1015 }
1016
1017 // Check the result
1018 verifyLoanStatus.checkPayment(
1019 state.loanScale,
1020 borrower,
1021 borrowerBalanceBeforePayment,
1022 totalDueAmount,
1023 adjustment);
1024 }
1025
1026 if (showStepBalances)
1027 {
1028 auto const loanSle = env.le(loanKeylet);
1029 if (!BEAST_EXPECT(loanSle))
1030 {
1031 // No reason for this not to exist
1032 return;
1033 }
1034 auto const current = constructRoundedLoanState(loanSle);
1035 auto const errors = nextTrueState - current;
1036 log << currencyLabel << " Loan balances: "
1037 << "\n\tAmount taken: " << paymentComponents.trackedValueDelta
1038 << "\n\tTotal value: " << current.valueOutstanding
1039 << " (true: " << truncate(nextTrueState.valueOutstanding)
1040 << ", error: " << truncate(errors.total())
1041 << ")\n\tPrincipal: " << current.principalOutstanding
1042 << " (true: " << truncate(nextTrueState.principalOutstanding)
1043 << ", error: " << truncate(errors.principal)
1044 << ")\n\tInterest: " << current.interestDue
1045 << " (true: " << truncate(nextTrueState.interestDue)
1046 << ", error: " << truncate(errors.interest)
1047 << ")\n\tMgmt fee: " << current.managementFeeDue
1048 << " (true: " << truncate(nextTrueState.managementFeeDue)
1049 << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining "
1050 << loanSle->at(sfPaymentRemaining) << std::endl;
1051
1052 currentRoundedState = current;
1053 }
1054
1055 --state.paymentRemaining;
1056 state.previousPaymentDate = state.nextPaymentDate;
1057 if (paymentComponents.specialCase == detail::PaymentSpecialCase::final)
1058 {
1059 state.paymentRemaining = 0;
1060 state.nextPaymentDate = 0;
1061 }
1062 else
1063 {
1064 state.nextPaymentDate += state.paymentInterval;
1065 }
1066 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
1067 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
1068 state.totalValue -= paymentComponents.trackedValueDelta;
1069
1070 if (paymentParams.validateBalances)
1071 verifyLoanStatus(state);
1072
1073 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
1074 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
1075 totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta;
1076 totalInterestPaid += paymentComponents.trackedInterestPart();
1077 totalFeesPaid += serviceFee;
1078 ++totalPaymentsMade;
1079
1080 currentTrueState = nextTrueState;
1081 }
1082 validateBorrowerBalance();
1083
1084 // Loan is paid off
1085 BEAST_EXPECT(state.paymentRemaining == 0);
1086 BEAST_EXPECT(state.principalOutstanding == 0);
1087
1088 auto const initialInterestDue = initialState.totalValue -
1089 (initialState.principalOutstanding + initialState.managementFeeOutstanding);
1090 if (paymentParams.validateBalances)
1091 {
1092 // Make sure all the payments add up
1093 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
1094 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
1095 BEAST_EXPECT(
1096 totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
1097 // This is almost a tautology given the previous checks, but
1098 // check it anyway for completeness.
1099 BEAST_EXPECT(totalInterestPaid == initialInterestDue);
1100 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
1101 }
1102
1103 if (showStepBalances)
1104 {
1105 auto const loanSle = env.le(loanKeylet);
1106 if (!BEAST_EXPECT(loanSle))
1107 {
1108 // No reason for this not to exist
1109 return;
1110 }
1111 log << currencyLabel << " Total amounts paid: "
1112 << "\n\tTotal value: " << totalPaid.trackedValueDelta
1113 << " (initial: " << truncate(initialState.totalValue)
1114 << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta)
1115 << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta
1116 << " (initial: " << truncate(initialState.principalOutstanding) << ", error: "
1117 << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta)
1118 << ")\n\tInterest: " << totalInterestPaid
1119 << " (initial: " << truncate(initialInterestDue)
1120 << ", error: " << truncate(initialInterestDue - totalInterestPaid)
1121 << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta
1122 << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: "
1123 << truncate(
1124 initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta)
1125 << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl;
1126 }
1127 }
1128
1129 void
1131 AssetType assetType,
1132 BrokerParameters const& brokerParams,
1133 LoanParameters const& loanParams)
1134 {
1135 using namespace jtx;
1136
1137 Account const issuer("issuer");
1138 Account const lender("lender");
1139 Account const borrower("borrower");
1140
1141 Env env(*this, all);
1142
1143 auto loanResult =
1144 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
1145 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
1146 return;
1147
1148 auto broker = std::get<BrokerInfo>(*loanResult);
1149 auto loanKeylet = std::get<Keylet>(*loanResult);
1150 auto pseudoAcct = std::get<Account>(*loanResult);
1151
1152 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
1153
1155 env,
1156 broker,
1157 loanParams,
1158 loanKeylet,
1159 verifyLoanStatus,
1160 issuer,
1161 lender,
1162 borrower,
1164 }
1165
1176 void
1178 std::string const& caseLabel,
1179 char const* label,
1180 jtx::Env& env,
1181 Number const& loanAmount,
1182 int interestExponent,
1183 jtx::Account const& lender,
1184 jtx::Account const& borrower,
1185 jtx::Account const& evan,
1186 BrokerInfo const& broker,
1187 jtx::Account const& pseudoAcct,
1189 // The end of life callback is expected to take the loan to 0 payments
1190 // remaining, one way or another
1191 std::function<void(Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus)>
1192 toEndOfLife)
1193 {
1194 auto const [keylet, loanSequence] = [&]() {
1195 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
1196 if (!BEAST_EXPECT(brokerSle))
1197 {
1198 // will be invalid
1200 }
1201
1202 // Broker has no loans
1203 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1204
1205 // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_
1206 // object.
1207 auto const loanSequence = brokerSle->at(sfLoanSequence);
1208 return std::make_pair(keylet::loan(broker.brokerID, loanSequence), loanSequence);
1209 }();
1210
1211 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet);
1212
1213 // No loans yet
1214 verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0);
1215
1216 if (!BEAST_EXPECT(loanSequence != 0))
1217 return;
1218
1219 testcase << caseLabel << " " << label;
1220
1221 using namespace jtx;
1222 using namespace loan;
1223 using namespace std::chrono_literals;
1224
1225 auto applyExponent = [interestExponent, this](TenthBips32 value) mutable {
1226 BEAST_EXPECT(value > TenthBips32(0));
1227 while (interestExponent > 0)
1228 {
1229 auto const oldValue = value;
1230 value *= 10;
1231 --interestExponent;
1232 BEAST_EXPECT(value / 10 == oldValue);
1233 }
1234 while (interestExponent < 0)
1235 {
1236 auto const oldValue = value;
1237 value /= 10;
1238 ++interestExponent;
1239 BEAST_EXPECT(value * 10 == oldValue);
1240 }
1241 return value;
1242 };
1243
1244 auto const borrowerOwnerCount = env.ownerCount(borrower);
1245
1246 auto const loanSetFee = env.current()->fees().base * 2;
1247 LoanParameters const loanParams{
1248 .account = borrower,
1249 .counter = lender,
1250 .counterpartyExplicit = false,
1251 .principalRequest = loanAmount,
1252 .setFee = loanSetFee,
1253 .originationFee = 1,
1254 .serviceFee = 2,
1255 .lateFee = 3,
1256 .closeFee = 4,
1257 .overFee = applyExponent(percentageToTenthBips(5) / 10),
1258 .interest = applyExponent(percentageToTenthBips(12)),
1259 // 2.4%
1260 .lateInterest = applyExponent(percentageToTenthBips(24) / 10),
1261 .closeInterest = applyExponent(percentageToTenthBips(36) / 10),
1262 .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10),
1263 .payTotal = 12,
1264 .payInterval = 600,
1265 .gracePd = 60,
1266 .flags = flags,
1267 };
1268 Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value();
1269 auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value();
1270 auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value();
1271 auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value();
1272 auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value();
1273
1274 auto const borrowerStartbalance = env.balance(borrower, broker.asset);
1275
1276 auto createJtx = loanParams(env, broker);
1277 // Successfully create a Loan
1278 env(createJtx);
1279
1280 env.close();
1281
1282 auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count();
1283
1284 if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
1285 BEAST_EXPECT(brokerSle))
1286 {
1287 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1);
1288 }
1289
1290 {
1291 // Need to account for fees if the loan is in XRP
1292 PrettyAmount adjustment = broker.asset(0);
1293 if (broker.asset.native())
1294 {
1295 adjustment = 2 * env.current()->fees().base;
1296 }
1297
1298 BEAST_EXPECT(
1299 env.balance(borrower, broker.asset).value() ==
1300 borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount -
1301 adjustment.value());
1302 }
1303
1304 auto const loanFlags =
1305 createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0);
1306
1307 if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
1308 {
1309 // log << "loan after create: " << to_string(loan->getJson())
1310 // << std::endl;
1311 BEAST_EXPECT(
1312 loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment));
1313 BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence);
1314 BEAST_EXPECT(loan->at(sfBorrower) == borrower.id());
1315 BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID);
1316 BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount);
1317 BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount);
1318 BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount);
1319 BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount);
1320 BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee);
1321 BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest);
1322 BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest);
1323 BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest);
1324 BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest);
1325 BEAST_EXPECT(loan->at(sfStartDate) == startDate);
1326 BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval);
1327 BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd);
1328 BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0);
1329 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval);
1330 BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal);
1331 BEAST_EXPECT(
1332 loan->at(sfLoanScale) >=
1333 (broker.asset.integral()
1334 ? 0
1335 : std::max(broker.vaultScale(env), principalRequestAmount.exponent())));
1336 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount);
1337 }
1338
1339 auto state = getCurrentState(env, broker, keylet, verifyLoanStatus);
1340
1341 auto const loanProperties = computeLoanProperties(
1342 broker.asset.raw(),
1343 state.principalOutstanding,
1344 state.interestRate,
1345 state.paymentInterval,
1346 state.paymentRemaining,
1348 state.loanScale);
1349
1350 verifyLoanStatus(
1351 0,
1352 startDate + *loanParams.payInterval,
1353 *loanParams.payTotal,
1354 state.loanScale,
1355 loanProperties.loanState.valueOutstanding,
1356 principalRequestAmount,
1357 loanProperties.loanState.managementFeeDue,
1358 loanProperties.periodicPayment,
1359 loanFlags | 0);
1360
1361 // Manage the loan
1362 // no-op
1363 env(manage(lender, keylet.key, 0));
1364 {
1365 // no flags
1366 auto jt = manage(lender, keylet.key, 0);
1367 jt.removeMember(sfFlags.getName());
1368 env(jt);
1369 }
1370 // Only the lender can manage
1371 env(manage(evan, keylet.key, 0), ter(tecNO_PERMISSION));
1372 // unknown flags
1373 env(manage(lender, keylet.key, tfLoanManageMask), ter(temINVALID_FLAG));
1374 // combinations of flags are not allowed
1375 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), ter(temINVALID_FLAG));
1376 env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), ter(temINVALID_FLAG));
1377 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), ter(temINVALID_FLAG));
1378 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault),
1380 // invalid loan ID
1381 env(manage(lender, broker.brokerID, tfLoanImpair), ter(tecNO_ENTRY));
1382 // Loan is unimpaired, can't unimpair it again
1383 env(manage(lender, keylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION));
1384 // Loan is unimpaired, it can go into default, but only after it's past
1385 // due
1386 env(manage(lender, keylet.key, tfLoanDefault), ter(tecTOO_SOON));
1387
1388 // Check the vault
1389 bool const canImpair = canImpairLoan(env, broker, state);
1390 // Impair the loan, if possible
1391 env(manage(lender, keylet.key, tfLoanImpair),
1392 canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED));
1393 // Unimpair the loan
1394 env(manage(lender, keylet.key, tfLoanUnimpair),
1395 canImpair ? ter(tesSUCCESS) : ter(tecNO_PERMISSION));
1396
1397 auto const nextDueDate = startDate + *loanParams.payInterval;
1398
1399 env.close();
1400
1401 verifyLoanStatus(
1402 0,
1403 nextDueDate,
1404 *loanParams.payTotal,
1405 loanProperties.loanScale,
1406 loanProperties.loanState.valueOutstanding,
1407 principalRequestAmount,
1408 loanProperties.loanState.managementFeeDue,
1409 loanProperties.periodicPayment,
1410 loanFlags | 0);
1411
1412 // Can't delete the loan yet. It has payments remaining.
1413 env(del(lender, keylet.key), ter(tecHAS_OBLIGATIONS));
1414
1415 if (BEAST_EXPECT(toEndOfLife))
1416 toEndOfLife(keylet, verifyLoanStatus);
1417 env.close();
1418
1419 // Verify the loan is at EOL
1420 if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
1421 {
1422 BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
1423 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
1424 }
1425 auto const borrowerStartingBalance = env.balance(borrower, broker.asset);
1426
1427 // Try to delete the loan broker with an active loan
1428 env(loanBroker::del(lender, broker.brokerID), ter(tecHAS_OBLIGATIONS));
1429 // Ensure the above tx doesn't get ordered after the LoanDelete and
1430 // delete our broker!
1431 env.close();
1432
1433 // Test failure cases
1434 env(del(lender, keylet.key, tfLoanOverpayment), ter(temINVALID_FLAG));
1435 env(del(evan, keylet.key), ter(tecNO_PERMISSION));
1436 env(del(lender, broker.brokerID), ter(tecNO_ENTRY));
1437
1438 // Delete the loan
1439 // Either the borrower or the lender can delete the loan. Alternate
1440 // between who does it across tests.
1441 static unsigned deleteCounter = 0;
1442 auto const deleter = ((++deleteCounter % 2) != 0u) ? lender : borrower;
1443 env(del(deleter, keylet.key));
1444 env.close();
1445
1446 PrettyAmount adjustment = broker.asset(0);
1447 if (deleter == borrower)
1448 {
1449 // Need to account for fees if the loan is in XRP
1450 if (broker.asset.native())
1451 {
1452 adjustment = env.current()->fees().base;
1453 }
1454 }
1455
1456 // No loans left
1457 verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0);
1458
1459 BEAST_EXPECT(
1460 env.balance(borrower, broker.asset).value() ==
1461 borrowerStartingBalance.value() - adjustment);
1462 BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
1463
1464 if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
1465 BEAST_EXPECT(brokerSle))
1466 {
1467 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1468 }
1469 }
1470
1471 static std::string
1473 {
1474 if (asset.native())
1475 return "XRP";
1476 if (asset.holds<Issue>())
1477 return "IOU";
1478 if (asset.holds<MPTIssue>())
1479 return "MPT";
1480 return "Unknown";
1481 }
1482
1491 template <class TAsset, std::size_t NAsset>
1492 void
1494 jtx::Env& env,
1495 jtx::MPTTester& mptt,
1496 std::array<TAsset, NAsset> const& assets,
1497 BrokerInfo const& broker,
1498 Number const& loanAmount,
1499 int interestExponent)
1500 {
1501 using namespace jtx;
1502 using namespace Lending;
1503
1504 auto const& asset = broker.asset.raw();
1505 auto const currencyLabel = getCurrencyLabel(asset);
1506 auto const caseLabel = [&]() {
1508 ss << "Lifecycle: " << loanAmount << " " << currencyLabel
1509 << " Scale interest to: " << interestExponent << " ";
1510 return ss.str();
1511 }();
1512 testcase << caseLabel;
1513
1514 using namespace loan;
1515 using namespace std::chrono_literals;
1516 using d = NetClock::duration;
1517 using tp = NetClock::time_point;
1518
1519 Account const issuer{"issuer"};
1520 // For simplicity, lender will be the sole actor for the vault &
1521 // brokers.
1522 Account const lender{"lender"};
1523 // Borrower only wants to borrow
1524 Account const borrower{"borrower"};
1525 // Evan will attempt to be naughty
1526 Account const evan{"evan"};
1527 // Do not fund alice
1528 Account const alice{"alice"};
1529
1530 Number const principalRequest = broker.asset(loanAmount).value();
1531 Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0);
1532 BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10);
1533 Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value();
1534 Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value();
1535 Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value();
1536
1537 auto const loanSetFee = fee(env.current()->fees().base * 2);
1538
1539 auto const pseudoAcct = [&]() {
1540 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
1541 if (!BEAST_EXPECT(brokerSle))
1542 return Account{lender};
1543 auto const brokerPseudo = brokerSle->at(sfAccount);
1544 return Account("Broker pseudo-account", brokerPseudo);
1545 }();
1546
1547 auto const baseFee = env.current()->fees().base;
1548
1549 auto badKeylet = keylet::vault(lender.id(), env.seq(lender));
1550 // Try some failure cases
1551 // flags are checked first
1552 env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask),
1553 sig(sfCounterpartySignature, lender),
1554 loanSetFee,
1556
1557 // field length validation
1558 // sfData: good length, bad account
1559 env(set(evan, broker.brokerID, principalRequest),
1560 sig(sfCounterpartySignature, borrower),
1562 loanSetFee,
1563 ter(tefBAD_AUTH));
1564 // sfData: too long
1565 env(set(evan, broker.brokerID, principalRequest),
1566 sig(sfCounterpartySignature, lender),
1568 loanSetFee,
1569 ter(temINVALID));
1570
1571 // field range validation
1572 // sfOverpaymentFee: good value, bad account
1573 env(set(evan, broker.brokerID, principalRequest),
1574 sig(sfCounterpartySignature, borrower),
1575 overpaymentFee(maxOverpaymentFee),
1576 loanSetFee,
1577 ter(tefBAD_AUTH));
1578 // sfOverpaymentFee: too big
1579 env(set(evan, broker.brokerID, principalRequest),
1580 sig(sfCounterpartySignature, lender),
1581 overpaymentFee(maxOverpaymentFee + 1),
1582 loanSetFee,
1583 ter(temINVALID));
1584
1585 // sfInterestRate: good value, bad account
1586 env(set(evan, broker.brokerID, principalRequest),
1587 sig(sfCounterpartySignature, borrower),
1588 interestRate(maxInterestRate),
1589 loanSetFee,
1590 ter(tefBAD_AUTH));
1591 env(set(evan, broker.brokerID, principalRequest),
1592 sig(sfCounterpartySignature, borrower),
1593 interestRate(TenthBips32(0)),
1594 loanSetFee,
1595 ter(tefBAD_AUTH));
1596 // sfInterestRate: too big
1597 env(set(evan, broker.brokerID, principalRequest),
1598 sig(sfCounterpartySignature, lender),
1599 interestRate(maxInterestRate + 1),
1600 loanSetFee,
1601 ter(temINVALID));
1602 // sfInterestRate: too small
1603 env(set(evan, broker.brokerID, principalRequest),
1604 sig(sfCounterpartySignature, lender),
1605 interestRate(TenthBips32(-1)),
1606 loanSetFee,
1607 ter(temINVALID));
1608
1609 // sfLateInterestRate: good value, bad account
1610 env(set(evan, broker.brokerID, principalRequest),
1611 sig(sfCounterpartySignature, borrower),
1612 lateInterestRate(maxLateInterestRate),
1613 loanSetFee,
1614 ter(tefBAD_AUTH));
1615 env(set(evan, broker.brokerID, principalRequest),
1616 sig(sfCounterpartySignature, borrower),
1617 lateInterestRate(TenthBips32(0)),
1618 loanSetFee,
1619 ter(tefBAD_AUTH));
1620 // sfLateInterestRate: too big
1621 env(set(evan, broker.brokerID, principalRequest),
1622 sig(sfCounterpartySignature, lender),
1623 lateInterestRate(maxLateInterestRate + 1),
1624 loanSetFee,
1625 ter(temINVALID));
1626 // sfLateInterestRate: too small
1627 env(set(evan, broker.brokerID, principalRequest),
1628 sig(sfCounterpartySignature, lender),
1629 lateInterestRate(TenthBips32(-1)),
1630 loanSetFee,
1631 ter(temINVALID));
1632
1633 // sfCloseInterestRate: good value, bad account
1634 env(set(evan, broker.brokerID, principalRequest),
1635 sig(sfCounterpartySignature, borrower),
1636 closeInterestRate(maxCloseInterestRate),
1637 loanSetFee,
1638 ter(tefBAD_AUTH));
1639 env(set(evan, broker.brokerID, principalRequest),
1640 sig(sfCounterpartySignature, borrower),
1641 closeInterestRate(TenthBips32(0)),
1642 loanSetFee,
1643 ter(tefBAD_AUTH));
1644 // sfCloseInterestRate: too big
1645 env(set(evan, broker.brokerID, principalRequest),
1646 sig(sfCounterpartySignature, lender),
1647 closeInterestRate(maxCloseInterestRate + 1),
1648 loanSetFee,
1649 ter(temINVALID));
1650 env(set(evan, broker.brokerID, principalRequest),
1651 sig(sfCounterpartySignature, lender),
1652 closeInterestRate(TenthBips32(-1)),
1653 loanSetFee,
1654 ter(temINVALID));
1655
1656 // sfOverpaymentInterestRate: good value, bad account
1657 env(set(evan, broker.brokerID, principalRequest),
1658 sig(sfCounterpartySignature, borrower),
1659 overpaymentInterestRate(maxOverpaymentInterestRate),
1660 loanSetFee,
1661 ter(tefBAD_AUTH));
1662 env(set(evan, broker.brokerID, principalRequest),
1663 sig(sfCounterpartySignature, borrower),
1664 overpaymentInterestRate(TenthBips32(0)),
1665 loanSetFee,
1666 ter(tefBAD_AUTH));
1667 // sfOverpaymentInterestRate: too big
1668 env(set(evan, broker.brokerID, principalRequest),
1669 sig(sfCounterpartySignature, lender),
1670 overpaymentInterestRate(maxOverpaymentInterestRate + 1),
1671 loanSetFee,
1672 ter(temINVALID));
1673 env(set(evan, broker.brokerID, principalRequest),
1674 sig(sfCounterpartySignature, lender),
1675 overpaymentInterestRate(TenthBips32(-1)),
1676 loanSetFee,
1677 ter(temINVALID));
1678
1679 // sfPaymentTotal: good value, bad account
1680 env(set(evan, broker.brokerID, principalRequest),
1681 sig(sfCounterpartySignature, borrower),
1682 paymentTotal(LoanSet::minPaymentTotal),
1683 loanSetFee,
1684 ter(tefBAD_AUTH));
1685 // sfPaymentTotal: too small (there is no max)
1686 env(set(evan, broker.brokerID, principalRequest),
1687 sig(sfCounterpartySignature, lender),
1688 paymentTotal(LoanSet::minPaymentTotal - 1),
1689 loanSetFee,
1690 ter(temINVALID));
1691
1692 // sfPaymentInterval: good value, bad account
1693 env(set(evan, broker.brokerID, principalRequest),
1694 sig(sfCounterpartySignature, borrower),
1695 paymentInterval(LoanSet::minPaymentInterval),
1696 loanSetFee,
1697 ter(tefBAD_AUTH));
1698 // sfPaymentInterval: too small (there is no max)
1699 env(set(evan, broker.brokerID, principalRequest),
1700 sig(sfCounterpartySignature, lender),
1701 paymentInterval(LoanSet::minPaymentInterval - 1),
1702 loanSetFee,
1703 ter(temINVALID));
1704
1705 // sfGracePeriod: good value, bad account
1706 env(set(evan, broker.brokerID, principalRequest),
1707 sig(sfCounterpartySignature, borrower),
1708 paymentInterval(LoanSet::minPaymentInterval * 2),
1709 gracePeriod(LoanSet::minPaymentInterval * 2),
1710 loanSetFee,
1711 ter(tefBAD_AUTH));
1712 // sfGracePeriod: larger than paymentInterval
1713 env(set(evan, broker.brokerID, principalRequest),
1714 sig(sfCounterpartySignature, lender),
1715 paymentInterval(LoanSet::minPaymentInterval * 2),
1716 gracePeriod(LoanSet::minPaymentInterval * 3),
1717 loanSetFee,
1718 ter(temINVALID));
1719
1720 // insufficient fee - single sign
1721 env(set(borrower, broker.brokerID, principalRequest),
1722 sig(sfCounterpartySignature, lender),
1724 // insufficient fee - multisign
1725 env(signers(lender, 2, {{evan, 1}, {borrower, 1}}));
1726 env(signers(borrower, 2, {{evan, 1}, {lender, 1}}));
1727 env(set(borrower, broker.brokerID, principalRequest),
1728 counterparty(lender),
1729 msig(evan, lender),
1730 msig(sfCounterpartySignature, evan, borrower),
1731 fee(env.current()->fees().base * 5 - 1),
1733 // Bad multisign signatures for borrower (Account)
1734 env(set(borrower, broker.brokerID, principalRequest),
1735 counterparty(lender),
1736 msig(alice, issuer),
1737 msig(sfCounterpartySignature, evan, borrower),
1738 fee(env.current()->fees().base * 5),
1740 // Bad multisign signatures for issuer (Counterparty)
1741 env(set(borrower, broker.brokerID, principalRequest),
1742 counterparty(lender),
1743 msig(evan, lender),
1744 msig(sfCounterpartySignature, alice, issuer),
1745 fee(env.current()->fees().base * 5 - 1),
1747 env(signers(lender, none));
1748 env(signers(borrower, none));
1749 // multisign sufficient fee, but no signers set up
1750 env(set(borrower, broker.brokerID, principalRequest),
1751 counterparty(lender),
1752 msig(evan, lender),
1753 msig(sfCounterpartySignature, evan, borrower),
1754 fee(env.current()->fees().base * 5),
1756 // not the broker owner, no counterparty, not signed by broker
1757 // owner
1758 env(set(borrower, broker.brokerID, principalRequest),
1759 sig(sfCounterpartySignature, evan),
1760 loanSetFee,
1761 ter(tefBAD_AUTH));
1762 // not the broker owner, counterparty is borrower
1763 env(set(evan, broker.brokerID, principalRequest),
1764 counterparty(borrower),
1765 sig(sfCounterpartySignature, borrower),
1766 loanSetFee,
1768 // not a LoanBroker object, no counterparty
1769 env(set(lender, badKeylet.key, principalRequest),
1770 sig(sfCounterpartySignature, evan),
1771 loanSetFee,
1773 // not a LoanBroker object, counterparty is valid
1774 env(set(lender, badKeylet.key, principalRequest),
1775 counterparty(borrower),
1776 sig(sfCounterpartySignature, borrower),
1777 loanSetFee,
1778 ter(tecNO_ENTRY));
1779 // borrower doesn't exist
1780 env(set(lender, broker.brokerID, principalRequest),
1781 counterparty(alice),
1782 sig(sfCounterpartySignature, alice),
1783 loanSetFee,
1785
1786 // Request more funds than the vault has available
1787 env(set(evan, broker.brokerID, totalVaultRequest + 1),
1788 sig(sfCounterpartySignature, lender),
1789 loanSetFee,
1791
1792 // Request more funds than the broker's first-loss capital can
1793 // cover.
1794 env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1),
1795 sig(sfCounterpartySignature, lender),
1796 loanSetFee,
1798
1799 // Frozen trust line / locked MPT issuance
1800 // XRP can not be frozen, but run through the loop anyway to test
1801 // the tecLIMIT_EXCEEDED case
1802 {
1803 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
1804 if (!BEAST_EXPECT(brokerSle))
1805 return;
1806
1807 auto const vaultPseudo = [&]() {
1808 auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
1809 if (!BEAST_EXPECT(vaultSle))
1810 {
1811 // This will be wrong, but the test has failed anyway.
1812 return Account{lender};
1813 }
1814 auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount));
1815 return vaultPseudo;
1816 }();
1817
1818 auto const [freeze, deepfreeze, unfreeze, expectedResult] =
1819 [&]() -> std::tuple<
1820 std::function<void(Account const& holder)>,
1821 std::function<void(Account const& holder)>,
1822 std::function<void(Account const& holder)>,
1823 TER> {
1824 // Freeze / lock the asset
1825 std::function<void(Account const& holder)> const empty;
1826 if (broker.asset.native())
1827 {
1828 // XRP can't be frozen
1829 return std::make_tuple(empty, empty, empty, tesSUCCESS);
1830 }
1831 if (broker.asset.holds<Issue>())
1832 {
1833 auto freeze = [&](Account const& holder) {
1834 env(trust(issuer, holder[iouCurrency](0), tfSetFreeze));
1835 };
1836 auto deepfreeze = [&](Account const& holder) {
1837 env(trust(issuer, holder[iouCurrency](0), tfSetFreeze | tfSetDeepFreeze));
1838 };
1839 auto unfreeze = [&](Account const& holder) {
1840 env(trust(
1841 issuer, holder[iouCurrency](0), tfClearFreeze | tfClearDeepFreeze));
1842 };
1843 return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN);
1844 }
1845
1846 auto freeze = [&](Account const& holder) {
1847 mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock});
1848 };
1849 auto unfreeze = [&](Account const& holder) {
1850 mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock});
1851 };
1852 return std::make_tuple(freeze, empty, unfreeze, tecLOCKED);
1853 }();
1854
1855 // Try freezing the accounts that can't be frozen
1856 if (freeze)
1857 {
1858 for (auto const& account : {vaultPseudo, evan})
1859 {
1860 // Freeze the account
1861 freeze(account);
1862
1863 // Try to create a loan with a frozen line
1864 env(set(evan, broker.brokerID, debtMaximumRequest),
1865 sig(sfCounterpartySignature, lender),
1866 loanSetFee,
1867 ter(expectedResult));
1868
1869 // Unfreeze the account
1870 BEAST_EXPECT(unfreeze);
1871 unfreeze(account);
1872
1873 // Ensure the line is unfrozen with a request that is fine
1874 // except too it requests more principal than the broker can
1875 // carry
1876 env(set(evan, broker.brokerID, debtMaximumRequest + 1),
1877 sig(sfCounterpartySignature, lender),
1878 loanSetFee,
1880 }
1881 }
1882
1883 // Deep freeze the borrower, which prevents them from receiving
1884 // funds
1885 if (deepfreeze)
1886 {
1887 // Make sure evan has a trust line that so the issuer can
1888 // freeze it. (Don't need to do this for the borrower,
1889 // because LoanSet will create a line to the borrower
1890 // automatically.)
1891 env(trust(evan, issuer[iouCurrency](100'000)));
1892
1893 for (auto const& account : {// these accounts can't be frozen, which deep freeze
1894 // implies
1895 vaultPseudo,
1896 evan,
1897 // these accounts can't be deep frozen
1898 lender})
1899 {
1900 // Freeze evan
1901 deepfreeze(account);
1902
1903 // Try to create a loan with a deep frozen line
1904 env(set(evan, broker.brokerID, debtMaximumRequest),
1905 sig(sfCounterpartySignature, lender),
1906 loanSetFee,
1907 ter(expectedResult));
1908
1909 // Unfreeze evan
1910 BEAST_EXPECT(unfreeze);
1911 unfreeze(account);
1912
1913 // Ensure the line is unfrozen with a request that is fine
1914 // except too it requests more principal than the broker can
1915 // carry
1916 env(set(evan, broker.brokerID, debtMaximumRequest + 1),
1917 sig(sfCounterpartySignature, lender),
1918 loanSetFee,
1920 }
1921 }
1922 }
1923
1924 // Finally! Create a loan
1925
1926 auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) {
1927 if (auto const brokerSle = env.le(keylet::loanbroker(brokerID));
1928 BEAST_EXPECT(brokerSle))
1929 {
1930 auto const available = brokerSle->at(sfCoverAvailable);
1931 BEAST_EXPECT(available == expected);
1932 return available;
1933 }
1934 return Number{};
1935 };
1936 auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) {
1937 if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
1938 BEAST_EXPECT(brokerSle))
1939 {
1940 BEAST_EXPECT(
1941 state.loanScale >=
1942 (broker.asset.integral()
1943 ? 0
1944 : std::max(
1945 broker.vaultScale(env), state.principalOutstanding.exponent())));
1947 auto const defaultAmount = roundToAsset(
1948 broker.asset,
1949 std::min(
1952 brokerSle->at(sfDebtTotal), broker.params.coverRateMin),
1954 state.totalValue - state.managementFeeOutstanding),
1955 state.loanScale);
1956 return std::make_pair(defaultAmount, brokerSle->at(sfOwner));
1957 }
1958 return std::make_pair(Number{}, AccountID{});
1959 };
1960 auto replenishCover = [&env, &coverAvailable](
1961 BrokerInfo const& broker,
1962 AccountID const& brokerAcct,
1963 Number const& startingCoverAvailable,
1964 Number const& amountToBeCovered) {
1965 coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered);
1967 brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered}));
1968 coverAvailable(broker.brokerID, startingCoverAvailable);
1969 env.close();
1970 };
1971
1972 auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) {
1973 return [&, impair, baseFlag](
1974 Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
1975 // toEndOfLife
1976 //
1977 // Default the loan
1978
1979 // Initialize values with the current state
1980 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
1981 BEAST_EXPECT(state.flags == baseFlag);
1982
1983 auto const& broker = verifyLoanStatus.broker;
1984 auto const startingCoverAvailable = coverAvailable(
1985 broker.brokerID, broker.asset(broker.params.coverDeposit).number());
1986
1987 if (impair)
1988 {
1989 // Check the vault
1990 bool const canImpair = canImpairLoan(env, broker, state);
1991 // Impair the loan, if possible
1992 env(manage(lender, loanKeylet.key, tfLoanImpair),
1993 canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED));
1994
1995 if (canImpair)
1996 {
1997 state.flags |= tfLoanImpair;
1998 state.nextPaymentDate = env.now().time_since_epoch().count();
1999
2000 // Once the loan is impaired, it can't be impaired again
2001 env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION));
2002 }
2003 verifyLoanStatus(state);
2004 }
2005
2006 auto const nextDueDate = tp{d{state.nextPaymentDate}};
2007
2008 // Can't default the loan yet. The grace period hasn't
2009 // expired
2010 env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecTOO_SOON));
2011
2012 // Let some time pass so that the loan can be
2013 // defaulted
2014 env.close(nextDueDate + 60s);
2015
2016 auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker);
2017
2018 // Default the loan
2019 env(manage(lender, loanKeylet.key, tfLoanDefault));
2020 env.close();
2021
2022 // The LoanBroker just lost some of it's first-loss capital.
2023 // Replenish it.
2024 replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered);
2025
2026 state.flags |= tfLoanDefault;
2027 state.paymentRemaining = 0;
2028 state.totalValue = 0;
2029 state.principalOutstanding = 0;
2030 state.managementFeeOutstanding = 0;
2031 state.nextPaymentDate = 0;
2032 verifyLoanStatus(state);
2033
2034 // Once a loan is defaulted, it can't be managed
2035 env(manage(lender, loanKeylet.key, tfLoanUnimpair), ter(tecNO_PERMISSION));
2036 env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION));
2037 // Can't make a payment on it either
2038 env(pay(borrower, loanKeylet.key, broker.asset(300)), ter(tecKILLED));
2039 };
2040 };
2041
2042 auto singlePayment = [&](Keylet const& loanKeylet,
2043 VerifyLoanStatus const& verifyLoanStatus,
2044 LoanState& state,
2045 STAmount const& payoffAmount,
2046 std::uint32_t numPayments,
2047 std::uint32_t baseFlag,
2048 std::uint32_t txFlags) {
2049 // toEndOfLife
2050 //
2051 verifyLoanStatus(state);
2052
2053 // Send some bogus pay transactions
2054 env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags),
2055 ter(temINVALID));
2056 // broker.asset(80) is less than a single payment, but all these
2057 // checks fail before that matters
2058 env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), ter(temBAD_AMOUNT));
2059 env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), ter(tecNO_ENTRY));
2060 env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), ter(tecNO_PERMISSION));
2061
2062 // TODO: Write a general "isFlag" function? See STObject::isFlag.
2063 // Maybe add a static overloaded member?
2064 if (!(state.flags & lsfLoanOverpayment))
2065 {
2066 // If the loan does not allow overpayments, send a payment that
2067 // tries to make an overpayment. Do not include `txFlags`, so we
2068 // don't end up duplicating the next test transaction.
2069 env(pay(borrower,
2070 loanKeylet.key,
2071 STAmount{broker.asset, state.periodicPayment * Number{15, -1}},
2072 tfLoanOverpayment),
2073 fee(XRPAmount{baseFee * (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}),
2075
2076 {
2077 env.disableFeature(fixSecurity3_1_3);
2078 env(pay(borrower,
2079 loanKeylet.key,
2080 STAmount{broker.asset, state.periodicPayment * Number{15, -1}},
2081 tfLoanOverpayment),
2082 fee(XRPAmount{
2083 baseFee * (Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}),
2085 env.enableFeature(fixSecurity3_1_3);
2086 }
2087 }
2088 // Try to send a payment marked as multiple mutually exclusive
2089 // payment types. Do not include `txFlags`, so we don't duplicate
2090 // the prior test transaction.
2091 env(pay(borrower,
2092 loanKeylet.key,
2093 broker.asset(state.periodicPayment * 2),
2094 tfLoanLatePayment | tfLoanFullPayment),
2096 env(pay(borrower,
2097 loanKeylet.key,
2098 broker.asset(state.periodicPayment * 2),
2099 tfLoanLatePayment | tfLoanOverpayment),
2101 env(pay(borrower,
2102 loanKeylet.key,
2103 broker.asset(state.periodicPayment * 2),
2104 tfLoanOverpayment | tfLoanFullPayment),
2106 env(pay(borrower,
2107 loanKeylet.key,
2108 broker.asset(state.periodicPayment * 2),
2109 tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment),
2111
2112 {
2113 auto const otherAsset =
2114 broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0];
2115 env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), ter(tecWRONG_ASSET));
2116 }
2117
2118 // Amount doesn't cover a single payment
2119 env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags),
2121
2122 // Get the balance after these failed transactions take
2123 // fees
2124 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
2125
2126 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2127 // Try to pay a little extra to show that it's _not_
2128 // taken
2129 auto const transactionAmount = payoffAmount + broker.asset(10);
2130
2131 // Send a transaction that tries to pay more than the borrowers's
2132 // balance
2133 XRPAmount const badFee{
2134 baseFee *
2135 (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment /
2136 loanPaymentsPerFeeIncrement +
2137 1)};
2138 env(pay(borrower,
2139 loanKeylet.key,
2140 STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2},
2141 txFlags),
2142 fee(badFee),
2144
2145 XRPAmount const goodFee{baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)};
2146 env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), fee(goodFee));
2147
2148 env.close();
2149
2150 // log << env.meta()->getJson() << std::endl;
2151
2152 // Need to account for fees if the loan is in XRP
2153 PrettyAmount adjustment = broker.asset(0);
2154 if (broker.asset.native())
2155 {
2156 adjustment = badFee + goodFee;
2157 }
2158
2159 state.paymentRemaining = 0;
2160 state.principalOutstanding = 0;
2161 state.totalValue = 0;
2162 state.managementFeeOutstanding = 0;
2163 state.previousPaymentDate =
2164 state.nextPaymentDate + (state.paymentInterval * (numPayments - 1));
2165 state.nextPaymentDate = 0;
2166 verifyLoanStatus(state);
2167
2168 verifyLoanStatus.checkPayment(
2169 state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment);
2170
2171 // Can't impair or default a paid off loan
2172 env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION));
2173 env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION));
2174 };
2175
2176 auto fullPayment = [&](std::uint32_t baseFlag) {
2177 return [&, baseFlag](
2178 Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2179 // toEndOfLife
2180 //
2181 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2182 env.close(state.startDate + 20s);
2183 auto const loanAge = (env.now() - state.startDate).count();
2184 BEAST_EXPECT(loanAge == 30);
2185
2186 // Full payoff amount will consist of
2187 // 1. principal outstanding (1000)
2188 // 2. accrued interest (at 12%)
2189 // 3. prepayment penalty (closeInterest at 3.6%)
2190 // 4. close payment fee (4)
2191 // Calculate these values without the helper functions
2192 // to verify they're working correctly The numbers in
2193 // the below BEAST_EXPECTs may not hold across assets.
2194 Number const interval = state.paymentInterval;
2195 auto const periodicRate = interval * Number(12, -2) / secondsInYear;
2196 BEAST_EXPECT(
2197 periodicRate == Number(2283105022831050228ULL, -24, Number::normalized{}));
2198 STAmount const principalOutstanding{broker.asset, state.principalOutstanding};
2199 STAmount const accruedInterest{
2200 broker.asset, state.principalOutstanding * periodicRate * loanAge / interval};
2201 BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19)));
2202 STAmount const prepaymentPenalty{
2203 broker.asset, state.principalOutstanding * Number(36, -3)};
2204 BEAST_EXPECT(prepaymentPenalty == broker.asset(36));
2205 STAmount const closePaymentFee = broker.asset(4);
2206 auto const payoffAmount = roundToScale(
2207 principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee,
2208 state.loanScale);
2209 BEAST_EXPECT(
2210 payoffAmount ==
2212 broker.asset,
2213 broker.asset(Number(1040000114155251, -12)).number(),
2214 state.loanScale));
2215
2216 // The terms of this loan actually make the early payoff
2217 // more expensive than just making payments
2218 BEAST_EXPECT(
2219 payoffAmount >
2220 state.paymentRemaining * (state.periodicPayment + broker.asset(2).value()));
2221
2222 singlePayment(
2223 loanKeylet,
2224 verifyLoanStatus,
2225 state,
2226 payoffAmount,
2227 1,
2228 baseFlag,
2229 tfLoanFullPayment);
2230 };
2231 };
2232
2233 auto combineAllPayments = [&](std::uint32_t baseFlag) {
2234 return
2235 [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2236 // toEndOfLife
2237 //
2238
2239 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2240 env.close();
2241
2242 BEAST_EXPECT(
2243 STAmount(broker.asset, state.periodicPayment) ==
2244 broker.asset(Number(8333457002039338267, -17)));
2245
2246 // Make all the payments in one transaction
2247 // service fee is 2
2248 auto const startingPayments = state.paymentRemaining;
2249 STAmount const payoffAmount = [&]() {
2250 NumberRoundModeGuard const mg(Number::upward);
2251 auto const rawPayoff =
2252 startingPayments * (state.periodicPayment + broker.asset(2).value());
2253 STAmount payoffAmount{broker.asset, rawPayoff};
2254 BEAST_EXPECTS(
2255 payoffAmount == broker.asset(Number(1024014840244721, -12)),
2256 to_string(payoffAmount));
2257 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2258
2259 payoffAmount = roundToScale(payoffAmount, state.loanScale);
2260
2261 return payoffAmount;
2262 }();
2263
2264 auto const totalPayoffValue =
2265 state.totalValue + startingPayments * broker.asset(2).value();
2266 STAmount const totalPayoffAmount{broker.asset, totalPayoffValue};
2267
2268 BEAST_EXPECTS(
2269 totalPayoffAmount == payoffAmount,
2270 "Payoff amount: " + to_string(payoffAmount) +
2271 ". Total Value: " + to_string(totalPayoffAmount));
2272
2273 singlePayment(
2274 loanKeylet,
2275 verifyLoanStatus,
2276 state,
2277 payoffAmount,
2278 state.paymentRemaining,
2279 baseFlag,
2280 0);
2281 };
2282 };
2283
2284 // There are a lot of fields that can be set on a loan, but most
2285 // of them only affect the "math" when a payment is made. The
2286 // only one that really affects behavior is the
2287 // `tfLoanOverpayment` flag.
2288 lifecycle(
2289 caseLabel,
2290 "Loan overpayment allowed - Impair and Default",
2291 env,
2292 loanAmount,
2293 interestExponent,
2294 lender,
2295 borrower,
2296 evan,
2297 broker,
2298 pseudoAcct,
2299 tfLoanOverpayment,
2300 defaultImmediately(lsfLoanOverpayment));
2301
2302 lifecycle(
2303 caseLabel,
2304 "Loan overpayment prohibited - Impair and Default",
2305 env,
2306 loanAmount,
2307 interestExponent,
2308 lender,
2309 borrower,
2310 evan,
2311 broker,
2312 pseudoAcct,
2313 0,
2314 defaultImmediately(0));
2315
2316 lifecycle(
2317 caseLabel,
2318 "Loan overpayment allowed - Default without Impair",
2319 env,
2320 loanAmount,
2321 interestExponent,
2322 lender,
2323 borrower,
2324 evan,
2325 broker,
2326 pseudoAcct,
2327 tfLoanOverpayment,
2328 defaultImmediately(lsfLoanOverpayment, false));
2329
2330 lifecycle(
2331 caseLabel,
2332 "Loan overpayment prohibited - Default without Impair",
2333 env,
2334 loanAmount,
2335 interestExponent,
2336 lender,
2337 borrower,
2338 evan,
2339 broker,
2340 pseudoAcct,
2341 0,
2342 defaultImmediately(0, false));
2343
2344 lifecycle(
2345 caseLabel,
2346 "Loan overpayment prohibited - Pay off immediately",
2347 env,
2348 loanAmount,
2349 interestExponent,
2350 lender,
2351 borrower,
2352 evan,
2353 broker,
2354 pseudoAcct,
2355 0,
2356 fullPayment(0));
2357
2358 lifecycle(
2359 caseLabel,
2360 "Loan overpayment allowed - Pay off immediately",
2361 env,
2362 loanAmount,
2363 interestExponent,
2364 lender,
2365 borrower,
2366 evan,
2367 broker,
2368 pseudoAcct,
2369 tfLoanOverpayment,
2370 fullPayment(lsfLoanOverpayment));
2371
2372 lifecycle(
2373 caseLabel,
2374 "Loan overpayment prohibited - Combine all payments",
2375 env,
2376 loanAmount,
2377 interestExponent,
2378 lender,
2379 borrower,
2380 evan,
2381 broker,
2382 pseudoAcct,
2383 0,
2384 combineAllPayments(0));
2385
2386 lifecycle(
2387 caseLabel,
2388 "Loan overpayment allowed - Combine all payments",
2389 env,
2390 loanAmount,
2391 interestExponent,
2392 lender,
2393 borrower,
2394 evan,
2395 broker,
2396 pseudoAcct,
2397 tfLoanOverpayment,
2398 combineAllPayments(lsfLoanOverpayment));
2399
2400 lifecycle(
2401 caseLabel,
2402 "Loan overpayment prohibited - Make payments",
2403 env,
2404 loanAmount,
2405 interestExponent,
2406 lender,
2407 borrower,
2408 evan,
2409 broker,
2410 pseudoAcct,
2411 0,
2412 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2413 // toEndOfLife
2414 //
2415 // Draw and make multiple payments
2416 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2417 BEAST_EXPECT(state.flags == 0);
2418 env.close();
2419
2420 verifyLoanStatus(state);
2421
2422 env.close(state.startDate + 20s);
2423 auto const loanAge = (env.now() - state.startDate).count();
2424 BEAST_EXPECT(loanAge == 30);
2425
2426 // Periodic payment amount will consist of
2427 // 1. principal outstanding (1000)
2428 // 2. interest interest rate (at 12%)
2429 // 3. payment interval (600s)
2430 // 4. loan service fee (2)
2431 // Calculate these values without the helper functions
2432 // to verify they're working correctly The numbers in
2433 // the below BEAST_EXPECTs may not hold across assets.
2434 Number const interval = state.paymentInterval;
2435 auto const periodicRate = interval * Number(12, -2) / secondsInYear;
2436 BEAST_EXPECT(
2437 periodicRate == Number(2283105022831050228, -24, Number::normalized{}));
2438 STAmount const roundedPeriodicPayment{
2439 broker.asset,
2440 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
2441
2442 testcase << currencyLabel << " Payment components: "
2443 << "Payments remaining, rawInterest, rawPrincipal, "
2444 "rawMFee, trackedValueDelta, trackedPrincipalDelta, "
2445 "trackedInterestDelta, trackedMgmtFeeDelta, special";
2446
2447 auto const serviceFee = broker.asset(2);
2448
2449 BEAST_EXPECT(
2450 roundedPeriodicPayment ==
2452 broker.asset(Number(8333457002039338267, -17), Number::upward),
2453 state.loanScale,
2455 // 83334570.01162141
2456 // Include the service fee
2457 STAmount const totalDue = roundToScale(
2458 roundedPeriodicPayment + serviceFee, state.loanScale, Number::upward);
2459 // Only check the first payment since the rounding
2460 // may drift as payments are made
2461 BEAST_EXPECT(
2462 totalDue ==
2464 broker.asset(Number(8533457002039338267, -17), Number::upward),
2465 state.loanScale,
2467
2468 {
2469 auto const raw = computeTheoreticalLoanState(
2470 state.periodicPayment,
2471 periodicRate,
2472 state.paymentRemaining,
2473 broker.params.managementFeeRate);
2474 auto const rounded = constructLoanState(
2475 state.totalValue,
2476 state.principalOutstanding,
2477 state.managementFeeOutstanding);
2478 testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining
2479 << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", "
2480 << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", "
2481 << rounded.principalOutstanding << ", " << rounded.interestDue << ", "
2482 << rounded.managementFeeDue;
2483 }
2484
2485 // Try to pay a little extra to show that it's _not_
2486 // taken
2487 STAmount const transactionAmount =
2488 STAmount{broker.asset, totalDue} + broker.asset(10);
2489 // Only check the first payment since the rounding
2490 // may drift as payments are made
2491 BEAST_EXPECT(
2492 transactionAmount ==
2494 broker.asset(Number(9533457002039400, -14), Number::upward),
2495 state.loanScale,
2497
2498 auto const initialState = state;
2499 detail::PaymentComponents totalPaid{
2500 .trackedValueDelta = 0,
2501 .trackedPrincipalDelta = 0,
2502 .trackedManagementFeeDelta = 0};
2503 Number totalInterestPaid = 0;
2504 std::size_t totalPaymentsMade = 0;
2505
2507 state.periodicPayment,
2508 periodicRate,
2509 state.paymentRemaining,
2510 broker.params.managementFeeRate);
2511
2512 while (state.paymentRemaining > 0)
2513 {
2514 // Compute the expected principal amount
2515 auto const paymentComponents = detail::computePaymentComponents(
2516 broker.asset.raw(),
2517 state.loanScale,
2518 state.totalValue,
2519 state.principalOutstanding,
2520 state.managementFeeOutstanding,
2521 state.periodicPayment,
2522 periodicRate,
2523 state.paymentRemaining,
2524 broker.params.managementFeeRate);
2525
2526 BEAST_EXPECTS(
2527 paymentComponents.specialCase == detail::PaymentSpecialCase::final ||
2528 paymentComponents.trackedValueDelta <= roundedPeriodicPayment,
2529 "Delta: " + to_string(paymentComponents.trackedValueDelta) +
2530 ", periodic payment: " + to_string(roundedPeriodicPayment));
2531
2532 xrpl::LoanState const nextTrueState = computeTheoreticalLoanState(
2533 state.periodicPayment,
2534 periodicRate,
2535 state.paymentRemaining - 1,
2536 broker.params.managementFeeRate);
2537 detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState;
2538
2539 testcase << currencyLabel << " Payment components: " << state.paymentRemaining
2540 << ", " << deltas.interest << ", " << deltas.principal << ", "
2541 << deltas.managementFee << ", " << paymentComponents.trackedValueDelta
2542 << ", " << paymentComponents.trackedPrincipalDelta << ", "
2543 << paymentComponents.trackedInterestPart() << ", "
2544 << paymentComponents.trackedManagementFeeDelta << ", "
2545 << [&]() -> char const* {
2546 if (paymentComponents.specialCase == detail::PaymentSpecialCase::final)
2547 return "final";
2548 if (paymentComponents.specialCase == detail::PaymentSpecialCase::extra)
2549 return "extra";
2550 return "none";
2551 }();
2552
2553 auto const totalDueAmount = STAmount{
2554 broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()};
2555
2556 // Due to the rounding algorithms to keep the interest and
2557 // principal in sync with "true" values, the computed amount
2558 // may be a little less than the rounded fixed payment
2559 // amount. For integral types, the difference should be < 3
2560 // (1 unit for each of the interest and management fee). For
2561 // IOUs, the difference should be after the 8th digit.
2562 Number const diff = totalDue - totalDueAmount;
2563 BEAST_EXPECT(
2564 paymentComponents.specialCase == detail::PaymentSpecialCase::final ||
2565 diff == beast::zero ||
2566 (diff > beast::zero &&
2567 ((broker.asset.integral() && (static_cast<Number>(diff) < 3)) ||
2568 (state.loanScale - diff.exponent() > 13))));
2569
2570 BEAST_EXPECT(
2571 paymentComponents.trackedValueDelta ==
2572 paymentComponents.trackedPrincipalDelta +
2573 paymentComponents.trackedInterestPart() +
2574 paymentComponents.trackedManagementFeeDelta);
2575 BEAST_EXPECT(
2576 paymentComponents.specialCase == detail::PaymentSpecialCase::final ||
2577 paymentComponents.trackedValueDelta <= roundedPeriodicPayment);
2578
2579 BEAST_EXPECT(
2580 state.paymentRemaining < 12 ||
2582 broker.asset, deltas.principal, state.loanScale, Number::upward) ==
2584 broker.asset(Number(8333228691531218890, -17), Number::upward),
2585 state.loanScale,
2587 BEAST_EXPECT(
2588 paymentComponents.trackedPrincipalDelta >= beast::zero &&
2589 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
2590 BEAST_EXPECT(
2591 paymentComponents.specialCase != detail::PaymentSpecialCase::final ||
2592 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
2593 BEAST_EXPECT(
2594 paymentComponents.specialCase == detail::PaymentSpecialCase::final ||
2595 (state.periodicPayment.exponent() -
2596 (deltas.principal + deltas.interest + deltas.managementFee -
2597 state.periodicPayment)
2598 .exponent()) > 14);
2599
2600 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
2601
2602 if (canImpairLoan(env, broker, state))
2603 {
2604 // Making a payment will unimpair the loan
2605 env(manage(lender, loanKeylet.key, tfLoanImpair));
2606 }
2607
2608 env.close();
2609
2610 // Make the payment
2611 env(pay(borrower, loanKeylet.key, transactionAmount));
2612
2613 env.close();
2614
2615 // Need to account for fees if the loan is in XRP
2616 PrettyAmount adjustment = broker.asset(0);
2617 if (broker.asset.native())
2618 {
2619 adjustment = env.current()->fees().base;
2620 }
2621
2622 // Check the result
2623 verifyLoanStatus.checkPayment(
2624 state.loanScale,
2625 borrower,
2626 borrowerBalanceBeforePayment,
2627 totalDueAmount,
2628 adjustment);
2629
2630 --state.paymentRemaining;
2631 state.previousPaymentDate = state.nextPaymentDate;
2632 if (paymentComponents.specialCase == detail::PaymentSpecialCase::final)
2633 {
2634 state.paymentRemaining = 0;
2635 state.nextPaymentDate = 0;
2636 }
2637 else
2638 {
2639 state.nextPaymentDate += state.paymentInterval;
2640 }
2641 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
2642 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
2643 state.totalValue -= paymentComponents.trackedValueDelta;
2644
2645 verifyLoanStatus(state);
2646
2647 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
2648 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
2649 totalPaid.trackedManagementFeeDelta +=
2650 paymentComponents.trackedManagementFeeDelta;
2651 totalInterestPaid += paymentComponents.trackedInterestPart();
2652 ++totalPaymentsMade;
2653
2654 currentTrueState = nextTrueState;
2655 }
2656
2657 // Loan is paid off
2658 BEAST_EXPECT(state.paymentRemaining == 0);
2659 BEAST_EXPECT(state.principalOutstanding == 0);
2660
2661 // Make sure all the payments add up
2662 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
2663 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
2664 BEAST_EXPECT(
2665 totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
2666 // This is almost a tautology given the previous checks, but
2667 // check it anyway for completeness.
2668 BEAST_EXPECT(
2669 totalInterestPaid ==
2670 initialState.totalValue -
2671 (initialState.principalOutstanding +
2672 initialState.managementFeeOutstanding));
2673 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
2674
2675 // Can't impair or default a paid off loan
2676 env(manage(lender, loanKeylet.key, tfLoanImpair), ter(tecNO_PERMISSION));
2677 env(manage(lender, loanKeylet.key, tfLoanDefault), ter(tecNO_PERMISSION));
2678 });
2679
2680#if LOANTODO
2681 // TODO
2682
2683 /*
2684 LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also
2685 borrower) tries to do the payment. Here's the scenario: Create a XRP
2686 loan with loan broker as borrower, loan origination fee and loan service
2687 fee. Loan broker makes the first payment with periodic payment and loan
2688 service fee.
2689 */
2690
2691 auto time = [&](std::string label, std::function<void()> timed) {
2692 if (!BEAST_EXPECT(timed))
2693 return;
2694
2696 using duration_type = std::chrono::milliseconds;
2697
2698 auto const start = clock_type::now();
2699 timed();
2700 auto const duration =
2701 std::chrono::duration_cast<duration_type>(clock_type::now() - start);
2702
2703 log << label << " took " << duration.count() << "ms" << std::endl;
2704
2705 return duration;
2706 };
2707
2708 lifecycle(
2709 caseLabel,
2710 "timing",
2711 env,
2712 loanAmount,
2713 interestExponent,
2714 lender,
2715 borrower,
2716 evan,
2717 broker,
2718 pseudoAcct,
2719 tfLoanOverpayment,
2720 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2721 // Estimate optimal values for loanPaymentsPerFeeIncrement and
2722 // loanMaximumPaymentsPerTransaction.
2723 using namespace loan;
2724
2725 auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet);
2726 auto const serviceFee = broker.asset(2).value();
2727
2728 STAmount const totalDue{
2729 broker.asset,
2731 broker.asset, state.periodicPayment + serviceFee, state.loanScale)};
2732
2733 // Make a single payment
2734 time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); });
2735 env.close();
2736
2737 // Make all but the final payment
2738 auto const numPayments = (state.paymentRemaining - 2);
2739 STAmount const bigPayment{broker.asset, totalDue * numPayments};
2740 XRPAmount const bigFee{baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)};
2741 time("ten payments", [&]() {
2742 env(pay(borrower, loanKeylet.key, bigPayment), fee(bigFee));
2743 });
2744 env.close();
2745
2746 time("final payment", [&]() {
2747 // Make the final payment
2748 env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1}));
2749 });
2750 env.close();
2751 });
2752
2753 lifecycle(
2754 caseLabel,
2755 "Loan overpayment allowed - Explicit overpayment",
2756 env,
2757 loanAmount,
2758 interestExponent,
2759 lender,
2760 borrower,
2761 evan,
2762 broker,
2763 pseudoAcct,
2764 tfLoanOverpayment,
2765 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2766
2767 lifecycle(
2768 caseLabel,
2769 "Loan overpayment prohibited - Late payment",
2770 env,
2771 loanAmount,
2772 interestExponent,
2773 lender,
2774 borrower,
2775 evan,
2776 broker,
2777 pseudoAcct,
2778 tfLoanOverpayment,
2779 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2780
2781 lifecycle(
2782 caseLabel,
2783 "Loan overpayment allowed - Late payment",
2784 env,
2785 loanAmount,
2786 interestExponent,
2787 lender,
2788 borrower,
2789 evan,
2790 broker,
2791 pseudoAcct,
2792 tfLoanOverpayment,
2793 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2794
2795 lifecycle(
2796 caseLabel,
2797 "Loan overpayment allowed - Late payment and overpayment",
2798 env,
2799 loanAmount,
2800 interestExponent,
2801 lender,
2802 borrower,
2803 evan,
2804 broker,
2805 pseudoAcct,
2806 tfLoanOverpayment,
2807 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2808
2809#endif
2810 }
2811
2812 void
2814 {
2815 using namespace jtx;
2816
2817 Account const issuer{"issuer"};
2818 Account const lender{"lender"};
2819 Account const borrower{"borrower"};
2820
2821 struct CaseArgs
2822 {
2823 bool requireAuth = false;
2824 bool authorizeBorrower = false;
2825 int initialXRP = 1'000'000;
2826 };
2827
2828 auto const testCase = [&, this](
2829 std::function<void(Env&, BrokerInfo const&, MPTTester&)> mptTest,
2830 std::function<void(Env&, BrokerInfo const&)> iouTest,
2831 CaseArgs args = {}) {
2832 Env env(*this, all);
2833 env.fund(XRP(args.initialXRP), issuer, lender, borrower);
2834 env.close();
2835 if (args.requireAuth)
2836 {
2837 env(fset(issuer, asfRequireAuth));
2838 env.close();
2839 }
2840
2841 // We need two different asset types, MPT and IOU. Prepare MPT
2842 // first
2843 MPTTester mptt{env, issuer, mptInitNoFund};
2844
2845 auto const none = LedgerSpecificFlags(0);
2846 mptt.create(
2847 {.flags = tfMPTCanTransfer | tfMPTCanLock |
2848 (args.requireAuth ? tfMPTRequireAuth : none)});
2849 env.close();
2850 PrettyAsset const mptAsset = mptt.issuanceID();
2851 mptt.authorize({.account = lender});
2852 mptt.authorize({.account = borrower});
2853 env.close();
2854 if (args.requireAuth)
2855 {
2856 mptt.authorize({.account = issuer, .holder = lender});
2857 if (args.authorizeBorrower)
2858 mptt.authorize({.account = issuer, .holder = borrower});
2859 env.close();
2860 }
2861
2862 env(pay(issuer, lender, mptAsset(10'000'000)));
2863 env.close();
2864
2865 // Prepare IOU
2866 PrettyAsset const iouAsset = issuer[iouCurrency];
2867 env(trust(lender, iouAsset(10'000'000)));
2868 env(trust(borrower, iouAsset(10'000'000)));
2869 env.close();
2870 if (args.requireAuth)
2871 {
2872 env(trust(issuer, iouAsset(0), lender, tfSetfAuth));
2873 env(pay(issuer, lender, iouAsset(10'000'000)));
2874 if (args.authorizeBorrower)
2875 {
2876 env(trust(issuer, iouAsset(0), borrower, tfSetfAuth));
2877 env(pay(issuer, borrower, iouAsset(10'000)));
2878 }
2879 }
2880 else
2881 {
2882 env(pay(issuer, lender, iouAsset(10'000'000)));
2883 env(pay(issuer, borrower, iouAsset(10'000)));
2884 }
2885 env.close();
2886
2887 // Create vaults and loan brokers
2888 std::array const assets{mptAsset, iouAsset};
2890 brokers.reserve(assets.size());
2891 for (auto const& asset : assets)
2892 {
2893 brokers.emplace_back(createVaultAndBroker(env, asset, lender));
2894 }
2895
2896 if (mptTest)
2897 (mptTest)(env, brokers[0], mptt);
2898 if (iouTest)
2899 (iouTest)(env, brokers[1]);
2900 };
2901
2902 testCase(
2903 [&, this](Env& env, BrokerInfo const& broker, auto&) {
2904 using namespace loan;
2905 Number const principalRequest = broker.asset(1'000).value();
2906
2907 testcase("MPT issuer is borrower, issuer submits");
2908 env(set(issuer, broker.brokerID, principalRequest),
2909 counterparty(lender),
2910 sig(sfCounterpartySignature, lender),
2911 fee(env.current()->fees().base * 5));
2912
2913 testcase("MPT issuer is borrower, lender submits");
2914 env(set(lender, broker.brokerID, principalRequest),
2915 counterparty(issuer),
2916 sig(sfCounterpartySignature, issuer),
2917 fee(env.current()->fees().base * 5));
2918 },
2919 [&, this](Env& env, BrokerInfo const& broker) {
2920 using namespace loan;
2921 Number const principalRequest = broker.asset(1'000).value();
2922
2923 testcase("IOU issuer is borrower, issuer submits");
2924 env(set(issuer, broker.brokerID, principalRequest),
2925 counterparty(lender),
2926 sig(sfCounterpartySignature, lender),
2927 fee(env.current()->fees().base * 5));
2928
2929 testcase("IOU issuer is borrower, lender submits");
2930 env(set(lender, broker.brokerID, principalRequest),
2931 counterparty(issuer),
2932 sig(sfCounterpartySignature, issuer),
2933 fee(env.current()->fees().base * 5));
2934 },
2935 CaseArgs{.requireAuth = true});
2936
2937 testCase(
2938 [&, this](Env& env, BrokerInfo const& broker, auto&) {
2939 using namespace loan;
2940 Number const principalRequest = broker.asset(1'000).value();
2941
2942 testcase("MPT unauthorized borrower, borrower submits");
2943 env(set(borrower, broker.brokerID, principalRequest),
2944 counterparty(lender),
2945 sig(sfCounterpartySignature, lender),
2946 fee(env.current()->fees().base * 5),
2947 ter{tecNO_AUTH});
2948
2949 testcase("MPT unauthorized borrower, lender submits");
2950 env(set(lender, broker.brokerID, principalRequest),
2951 counterparty(borrower),
2952 sig(sfCounterpartySignature, borrower),
2953 fee(env.current()->fees().base * 5),
2954 ter{tecNO_AUTH});
2955 },
2956 [&, this](Env& env, BrokerInfo const& broker) {
2957 using namespace loan;
2958 Number const principalRequest = broker.asset(1'000).value();
2959
2960 testcase("IOU unauthorized borrower, borrower submits");
2961 env(set(borrower, broker.brokerID, principalRequest),
2962 counterparty(lender),
2963 sig(sfCounterpartySignature, lender),
2964 fee(env.current()->fees().base * 5),
2965 ter{tecNO_AUTH});
2966
2967 testcase("IOU unauthorized borrower, lender submits");
2968 env(set(lender, broker.brokerID, principalRequest),
2969 counterparty(borrower),
2970 sig(sfCounterpartySignature, borrower),
2971 fee(env.current()->fees().base * 5),
2972 ter{tecNO_AUTH});
2973 },
2974 CaseArgs{.requireAuth = true});
2975
2976 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
2977 Env const env{*this, testable_amendments()};
2978 return {
2979 env.current()->fees().accountReserve(0).drops() / DROPS_PER_XRP.drops(),
2980 env.current()->fees().increment.drops() / DROPS_PER_XRP.drops()};
2981 }();
2982
2983 testCase(
2984 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
2985 using namespace loan;
2986 Number const principalRequest = broker.asset(1'000).value();
2987
2988 testcase(
2989 "MPT authorized borrower, borrower submits, borrower has "
2990 "no reserve");
2991 mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize});
2992 env.close();
2993
2994 auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower);
2995 auto const sleMPT1 = env.le(mptoken);
2996 BEAST_EXPECT(sleMPT1 == nullptr);
2997
2998 // Burn some XRP
2999 env(noop(borrower), fee(XRP((acctReserve * 2) + (incReserve * 2))));
3000 env.close();
3001
3002 // Cannot create loan, not enough reserve to create MPToken
3003 env(set(borrower, broker.brokerID, principalRequest),
3004 counterparty(lender),
3005 sig(sfCounterpartySignature, lender),
3006 fee(env.current()->fees().base * 5),
3007 ter{tecINSUFFICIENT_RESERVE});
3008 env.close();
3009
3010 // Can create loan now, will implicitly create MPToken
3011 env(pay(issuer, borrower, XRP(incReserve)));
3012 env.close();
3013 env(set(borrower, broker.brokerID, principalRequest),
3014 counterparty(lender),
3015 sig(sfCounterpartySignature, lender),
3016 fee(env.current()->fees().base * 5));
3017 env.close();
3018
3019 auto const sleMPT2 = env.le(mptoken);
3020 BEAST_EXPECT(sleMPT2 != nullptr);
3021 },
3022 {},
3023 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3024
3025 testCase(
3026 {},
3027 [&, this](Env& env, BrokerInfo const& broker) {
3028 using namespace loan;
3029 Number const principalRequest = broker.asset(1'000).value();
3030
3031 testcase(
3032 "IOU authorized borrower, borrower submits, borrower has "
3033 "no reserve");
3034 // Remove trust line from borrower to issuer
3035 env.trust(broker.asset(0), borrower);
3036 env.close();
3037
3038 env(pay(borrower, issuer, broker.asset(10'000)));
3039 env.close();
3040 auto const trustline = keylet::line(borrower, broker.asset.raw().get<Issue>());
3041 auto const sleLine1 = env.le(trustline);
3042 BEAST_EXPECT(sleLine1 == nullptr);
3043
3044 // Burn some XRP
3045 env(noop(borrower), fee(XRP((acctReserve * 2) + (incReserve * 2))));
3046 env.close();
3047
3048 // Cannot create loan, not enough reserve to create trust line
3049 env(set(borrower, broker.brokerID, principalRequest),
3050 counterparty(lender),
3051 sig(sfCounterpartySignature, lender),
3052 fee(env.current()->fees().base * 5),
3053 ter{tecNO_LINE_INSUF_RESERVE});
3054 env.close();
3055
3056 // Can create loan now, will implicitly create trust line
3057 env(pay(issuer, borrower, XRP(incReserve)));
3058 env.close();
3059 env(set(borrower, broker.brokerID, principalRequest),
3060 counterparty(lender),
3061 sig(sfCounterpartySignature, lender),
3062 fee(env.current()->fees().base * 5));
3063 env.close();
3064
3065 auto const sleLine2 = env.le(trustline);
3066 BEAST_EXPECT(sleLine2 != nullptr);
3067 },
3068 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3069
3070 testCase(
3071 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
3072 using namespace loan;
3073 Number const principalRequest = broker.asset(1'000).value();
3074
3075 testcase(
3076 "MPT authorized borrower, borrower submits, lender has "
3077 "no reserve");
3078 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
3079 auto const sleMPT1 = env.le(mptoken);
3080 BEAST_EXPECT(sleMPT1 != nullptr);
3081
3082 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
3083 env.close();
3084
3085 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
3086 env.close();
3087
3088 auto const sleMPT2 = env.le(mptoken);
3089 BEAST_EXPECT(sleMPT2 == nullptr);
3090
3091 // Burn some XRP
3092 env(noop(lender), fee(XRP(incReserve)));
3093 env.close();
3094
3095 // Cannot create loan, not enough reserve to create MPToken
3096 env(set(borrower, broker.brokerID, principalRequest),
3097 loanOriginationFee(broker.asset(1).value()),
3098 counterparty(lender),
3099 sig(sfCounterpartySignature, lender),
3100 fee(env.current()->fees().base * 5),
3101 ter{tecINSUFFICIENT_RESERVE});
3102 env.close();
3103
3104 // Can create loan now, will implicitly create MPToken
3105 env(pay(issuer, lender, XRP(incReserve)));
3106 env.close();
3107 env(set(borrower, broker.brokerID, principalRequest),
3108 loanOriginationFee(broker.asset(1).value()),
3109 counterparty(lender),
3110 sig(sfCounterpartySignature, lender),
3111 fee(env.current()->fees().base * 5));
3112 env.close();
3113
3114 auto const sleMPT3 = env.le(mptoken);
3115 BEAST_EXPECT(sleMPT3 != nullptr);
3116 },
3117 {},
3118 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3119
3120 testCase(
3121 {},
3122 [&, this](Env& env, BrokerInfo const& broker) {
3123 using namespace loan;
3124 Number const principalRequest = broker.asset(1'000).value();
3125
3126 testcase(
3127 "IOU authorized borrower, borrower submits, lender has no "
3128 "reserve");
3129 // Remove trust line from lender to issuer
3130 env.trust(broker.asset(0), lender);
3131 env.close();
3132
3133 auto const trustline = keylet::line(lender, broker.asset.raw().get<Issue>());
3134 auto const sleLine1 = env.le(trustline);
3135 BEAST_EXPECT(sleLine1 != nullptr);
3136
3137 env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value()))));
3138 env.close();
3139 auto const sleLine2 = env.le(trustline);
3140 BEAST_EXPECT(sleLine2 == nullptr);
3141
3142 // Burn some XRP
3143 env(noop(lender), fee(XRP(incReserve)));
3144 env.close();
3145
3146 // Cannot create loan, not enough reserve to create trust line
3147 env(set(borrower, broker.brokerID, principalRequest),
3148 loanOriginationFee(broker.asset(1).value()),
3149 counterparty(lender),
3150 sig(sfCounterpartySignature, lender),
3151 fee(env.current()->fees().base * 5),
3152 ter{tecNO_LINE_INSUF_RESERVE});
3153 env.close();
3154
3155 // Can create loan now, will implicitly create trust line
3156 env(pay(issuer, lender, XRP(incReserve)));
3157 env.close();
3158 env(set(borrower, broker.brokerID, principalRequest),
3159 loanOriginationFee(broker.asset(1).value()),
3160 counterparty(lender),
3161 sig(sfCounterpartySignature, lender),
3162 fee(env.current()->fees().base * 5));
3163 env.close();
3164
3165 auto const sleLine3 = env.le(trustline);
3166 BEAST_EXPECT(sleLine3 != nullptr);
3167 },
3168 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3169
3170 testCase(
3171 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
3172 using namespace loan;
3173 Number const principalRequest = broker.asset(1'000).value();
3174
3175 testcase("MPT authorized borrower, unauthorized lender");
3176 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
3177 auto const sleMPT1 = env.le(mptoken);
3178 BEAST_EXPECT(sleMPT1 != nullptr);
3179
3180 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
3181 env.close();
3182
3183 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
3184 env.close();
3185
3186 auto const sleMPT2 = env.le(mptoken);
3187 BEAST_EXPECT(sleMPT2 == nullptr);
3188
3189 // Cannot create loan, lender not authorized to receive fee
3190 env(set(borrower, broker.brokerID, principalRequest),
3191 loanOriginationFee(broker.asset(1).value()),
3192 counterparty(lender),
3193 sig(sfCounterpartySignature, lender),
3194 fee(env.current()->fees().base * 5),
3195 ter{tecNO_AUTH});
3196 env.close();
3197
3198 // Cannot create loan, even without an origination fee
3199 env(set(borrower, broker.brokerID, principalRequest),
3200 counterparty(lender),
3201 sig(sfCounterpartySignature, lender),
3202 fee(env.current()->fees().base * 5),
3203 ter{tecNO_AUTH});
3204 env.close();
3205
3206 // No MPToken for lender - no authorization and no payment
3207 auto const sleMPT3 = env.le(mptoken);
3208 BEAST_EXPECT(sleMPT3 == nullptr);
3209 },
3210 {},
3211 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3212
3213 testCase(
3214 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3215 using namespace loan;
3216 Number const principalRequest = broker.asset(1'000).value();
3217
3218 testcase("MPT authorized borrower, borrower submits");
3219 env(set(borrower, broker.brokerID, principalRequest),
3220 counterparty(lender),
3221 sig(sfCounterpartySignature, lender),
3222 fee(env.current()->fees().base * 5));
3223 },
3224 [&, this](Env& env, BrokerInfo const& broker) {
3225 using namespace loan;
3226 Number const principalRequest = broker.asset(1'000).value();
3227
3228 testcase("IOU authorized borrower, borrower submits");
3229 env(set(borrower, broker.brokerID, principalRequest),
3230 counterparty(lender),
3231 sig(sfCounterpartySignature, lender),
3232 fee(env.current()->fees().base * 5));
3233 },
3234 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3235
3236 testCase(
3237 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3238 using namespace loan;
3239 Number const principalRequest = broker.asset(1'000).value();
3240
3241 testcase("MPT authorized borrower, lender submits");
3242 env(set(lender, broker.brokerID, principalRequest),
3243 counterparty(borrower),
3244 sig(sfCounterpartySignature, borrower),
3245 fee(env.current()->fees().base * 5));
3246 },
3247 [&, this](Env& env, BrokerInfo const& broker) {
3248 using namespace loan;
3249 Number const principalRequest = broker.asset(1'000).value();
3250
3251 testcase("IOU authorized borrower, lender submits");
3252 env(set(lender, broker.brokerID, principalRequest),
3253 counterparty(borrower),
3254 sig(sfCounterpartySignature, borrower),
3255 fee(env.current()->fees().base * 5));
3256 },
3257 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3258
3259 jtx::Account const alice{"alice"};
3260 jtx::Account const bella{"bella"};
3261 auto const msigSetup = [&](Env& env, Account const& account) {
3262 Json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}});
3263 env(tx1);
3264 env.close();
3265 };
3266
3267 testCase(
3268 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3269 using namespace loan;
3270 msigSetup(env, lender);
3271 Number const principalRequest = broker.asset(1'000).value();
3272
3273 testcase(
3274 "MPT authorized borrower, borrower submits, lender "
3275 "multisign");
3276 env(set(borrower, broker.brokerID, principalRequest),
3277 counterparty(lender),
3278 msig(sfCounterpartySignature, alice, bella),
3279 fee(env.current()->fees().base * 5));
3280 },
3281 [&, this](Env& env, BrokerInfo const& broker) {
3282 using namespace loan;
3283 msigSetup(env, lender);
3284 Number const principalRequest = broker.asset(1'000).value();
3285
3286 testcase(
3287 "IOU authorized borrower, borrower submits, lender "
3288 "multisign");
3289 env(set(borrower, broker.brokerID, principalRequest),
3290 counterparty(lender),
3291 msig(sfCounterpartySignature, alice, bella),
3292 fee(env.current()->fees().base * 5));
3293 },
3294 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3295
3296 testCase(
3297 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3298 using namespace loan;
3299 msigSetup(env, borrower);
3300 Number const principalRequest = broker.asset(1'000).value();
3301
3302 testcase(
3303 "MPT authorized borrower, lender submits, borrower "
3304 "multisign");
3305 env(set(lender, broker.brokerID, principalRequest),
3306 counterparty(borrower),
3307 msig(sfCounterpartySignature, alice, bella),
3308 fee(env.current()->fees().base * 5));
3309 },
3310 [&, this](Env& env, BrokerInfo const& broker) {
3311 using namespace loan;
3312 msigSetup(env, borrower);
3313 Number const principalRequest = broker.asset(1'000).value();
3314
3315 testcase(
3316 "IOU authorized borrower, lender submits, borrower "
3317 "multisign");
3318 env(set(lender, broker.brokerID, principalRequest),
3319 counterparty(borrower),
3320 msig(sfCounterpartySignature, alice, bella),
3321 fee(env.current()->fees().base * 5));
3322 },
3323 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3324
3325 testCase(
3326 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3327 using namespace loan;
3328 Number const principalRequest = broker.asset(1'000).value();
3329 Vault const vault{env};
3330 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
3331 tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit;
3332 env(tx);
3333 env.close();
3334
3335 testcase("Vault at maximum value");
3336 env(set(issuer, broker.brokerID, principalRequest),
3337 counterparty(lender),
3338 interestRate(TenthBips32(10'000)),
3339 sig(sfCounterpartySignature, lender),
3340 fee(env.current()->fees().base * 5),
3342 },
3343 nullptr);
3344
3345 testCase(
3346 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3347 using namespace loan;
3348 Number const principalRequest = broker.asset(1'000).value();
3349 Vault const vault{env};
3350 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
3351 tx[sfAssetsMaximum] =
3352 BrokerParameters::defaults().vaultDeposit + broker.asset(1).number();
3353 env(tx);
3354 env.close();
3355
3356 testcase("Vault maximum value exceeded");
3357 env(set(issuer, broker.brokerID, principalRequest),
3358 counterparty(lender),
3359 interestRate(TenthBips32(100'000)),
3360 sig(sfCounterpartySignature, lender),
3361 fee(env.current()->fees().base * 5),
3362 paymentTotal(2),
3363 paymentInterval(3600 * 24),
3365 },
3366 nullptr);
3367 }
3368
3369 void
3371 {
3372 testcase("Lifecycle");
3373 using namespace jtx;
3374
3375 // Create 3 loan brokers: one for XRP, one for an IOU, and one for
3376 // an MPT. That'll require three corresponding SAVs.
3377 Env env(*this, all);
3378
3379 Account const issuer{"issuer"};
3380 // For simplicity, lender will be the sole actor for the vault &
3381 // brokers.
3382 Account const lender{"lender"};
3383 // Borrower only wants to borrow
3384 Account const borrower{"borrower"};
3385 // Evan will attempt to be naughty
3386 Account const evan{"evan"};
3387 // Do not fund alice
3388 Account const alice{"alice"};
3389
3390 // Fund the accounts and trust lines with the same amount so that
3391 // tests can use the same values regardless of the asset.
3392 env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan));
3393 env.close();
3394
3395 // Create assets
3396 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3397 PrettyAsset const iouAsset = issuer[iouCurrency];
3398 env(trust(lender, iouAsset(10'000'000)));
3399 env(trust(borrower, iouAsset(10'000'000)));
3400 env(trust(evan, iouAsset(10'000'000)));
3401 env(pay(issuer, evan, iouAsset(1'000'000)));
3402 env(pay(issuer, lender, iouAsset(10'000'000)));
3403 // Fund the borrower with enough to cover interest and fees
3404 env(pay(issuer, borrower, iouAsset(10'000)));
3405 env.close();
3406
3407 MPTTester mptt{env, issuer, mptInitNoFund};
3408 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
3409 // Scale the MPT asset a little bit so we can get some interest
3410 PrettyAsset const mptAsset{mptt.issuanceID(), 100};
3411 mptt.authorize({.account = lender});
3412 mptt.authorize({.account = borrower});
3413 mptt.authorize({.account = evan});
3414 env(pay(issuer, lender, mptAsset(10'000'000)));
3415 env(pay(issuer, evan, mptAsset(1'000'000)));
3416 // Fund the borrower with enough to cover interest and fees
3417 env(pay(issuer, borrower, mptAsset(10'000)));
3418 env.close();
3419
3420 std::array const assets{iouAsset, xrpAsset, mptAsset};
3421
3422 // Create vaults and loan brokers
3424 brokers.reserve(assets.size());
3425 for (auto const& asset : assets)
3426 {
3427 brokers.emplace_back(createVaultAndBroker(
3428 env, asset, lender, BrokerParameters{.data = "spam spam spam spam"}));
3429 }
3430
3431 // Create and update Loans
3432 for (auto const& broker : brokers)
3433 {
3434 for (int amountExponent = 3; amountExponent >= 3; --amountExponent)
3435 {
3436 Number const loanAmount{1, amountExponent};
3437 for (int interestExponent = 0; interestExponent >= 0; --interestExponent)
3438 {
3439 testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent);
3440 }
3441 }
3442
3443 if (auto brokerSle = env.le(keylet::loanbroker(broker.brokerID));
3444 BEAST_EXPECT(brokerSle))
3445 {
3446 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
3447 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
3448
3449 auto const coverAvailable = brokerSle->at(sfCoverAvailable);
3450 env(loanBroker::coverWithdraw(
3451 lender, broker.brokerID, STAmount(broker.asset, coverAvailable)));
3452 env.close();
3453
3454 brokerSle = env.le(keylet::loanbroker(broker.brokerID));
3455 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
3456 }
3457 // Verify we can delete the loan broker
3458 env(loanBroker::del(lender, broker.brokerID));
3459 env.close();
3460 }
3461 }
3462
3463 void
3465 {
3466 testcase << "Self Loan";
3467
3468 using namespace jtx;
3469 using namespace std::chrono_literals;
3470 // Create 3 loan brokers: one for XRP, one for an IOU, and one for
3471 // an MPT. That'll require three corresponding SAVs.
3472 Env env(*this, all);
3473
3474 Account const issuer{"issuer"};
3475 // For simplicity, lender will be the sole actor for the vault &
3476 // brokers.
3477 Account const lender{"lender"};
3478
3479 // Fund the accounts and trust lines with the same amount so that
3480 // tests can use the same values regardless of the asset.
3481 env.fund(XRP(100'000'000), issuer, noripple(lender));
3482 env.close();
3483
3484 // Use an XRP asset for simplicity
3485 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3486
3487 // Create vaults and loan brokers
3488 BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)};
3489
3490 using namespace loan;
3491
3492 auto const loanSetFee = fee(env.current()->fees().base * 2);
3493 Number const principalRequest{1, 3};
3494
3495 // The LoanSet json can be created without a counterparty signature,
3496 // but it will not pass preflight
3497 auto createJson = env.json(
3498 set(lender, broker.brokerID, broker.asset(principalRequest).value()), fee(loanSetFee));
3499 env(createJson, ter(temBAD_SIGNER));
3500
3501 // Adding an empty counterparty signature object also fails, but
3502 // at the RPC level.
3503 createJson = env.json(createJson, json(sfCounterpartySignature, Json::objectValue));
3504 env(createJson, ter(telENV_RPC_FAILED));
3505
3506 if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx))
3507 {
3508 Serializer s;
3509 jt.stx->add(s);
3510 auto const jr = env.rpc("submit", strHex(s.slice()));
3511
3512 BEAST_EXPECT(jr.isMember(jss::result));
3513 auto const jResult = jr[jss::result];
3514 BEAST_EXPECT(jResult[jss::error] == "invalidTransaction");
3515 BEAST_EXPECT(
3516 jResult[jss::error_exception] ==
3517 "fails local checks: Transaction has bad signature.");
3518 }
3519
3520 // Copy the transaction signature into the counterparty signature.
3521 Json::Value counterpartyJson{Json::objectValue};
3522 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
3523 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
3524 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
3525 counterpartyJson[sfSigners] = createJson[sfSigners];
3526
3527 // The duplicated signature works
3528 createJson = env.json(createJson, json(sfCounterpartySignature, counterpartyJson));
3529 env(createJson);
3530
3531 env.close();
3532
3533 auto const startDate = env.current()->header().parentCloseTime;
3534
3535 // Loan is successfully created
3536 {
3537 auto const res = env.rpc("account_objects", lender.human());
3538 auto const objects = res[jss::result][jss::account_objects];
3539
3541 BEAST_EXPECT(objects.size() == 4);
3542 for (auto const& object : objects)
3543 {
3544 ++types[object[sfLedgerEntryType].asString()];
3545 }
3546 BEAST_EXPECT(types.size() == 4);
3547 for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"})
3548 {
3549 BEAST_EXPECT(types[type] == 1);
3550 }
3551 }
3552 auto const loanID = [&]() {
3554 params[jss::account] = lender.human();
3555 params[jss::type] = "Loan";
3556 auto const res = env.rpc("json", "account_objects", to_string(params));
3557 auto const objects = res[jss::result][jss::account_objects];
3558
3559 BEAST_EXPECT(objects.size() == 1);
3560
3561 auto const loan = objects[0u];
3562 BEAST_EXPECT(loan[sfBorrower] == lender.human());
3563 // soeDEFAULT fields are not returned if they're in the default
3564 // state
3565 BEAST_EXPECT(!loan.isMember(sfCloseInterestRate));
3566 BEAST_EXPECT(!loan.isMember(sfClosePaymentFee));
3567 BEAST_EXPECT(loan[sfFlags] == 0);
3568 BEAST_EXPECT(loan[sfGracePeriod] == 60);
3569 BEAST_EXPECT(!loan.isMember(sfInterestRate));
3570 BEAST_EXPECT(!loan.isMember(sfLateInterestRate));
3571 BEAST_EXPECT(!loan.isMember(sfLatePaymentFee));
3572 BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID));
3573 BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee));
3574 BEAST_EXPECT(loan[sfLoanSequence] == 1);
3575 BEAST_EXPECT(!loan.isMember(sfLoanServiceFee));
3576 BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60);
3577 BEAST_EXPECT(!loan.isMember(sfOverpaymentFee));
3578 BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate));
3579 BEAST_EXPECT(loan[sfPaymentInterval] == 60);
3580 BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000");
3581 BEAST_EXPECT(loan[sfPaymentRemaining] == 1);
3582 BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate));
3583 BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000");
3584 BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000");
3585 BEAST_EXPECT(!loan.isMember(sfLoanScale));
3586 BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count());
3587
3588 return loan["index"].asString();
3589 }();
3590 auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})};
3591
3592 env.close(startDate);
3593
3594 // Make a payment
3595 env(pay(lender, loanKeylet.key, broker.asset(1000)));
3596 }
3597
3598 void
3600 {
3601 // From FIND-001
3602 testcase << "Batch Bypass Counterparty";
3603
3604 bool const lendingBatchEnabled = !std::any_of(
3605 Batch::disabledTxTypes.begin(), Batch::disabledTxTypes.end(), [](auto const& disabled) {
3606 return disabled == ttLOAN_BROKER_SET;
3607 });
3608
3609 using namespace jtx;
3610 using namespace std::chrono_literals;
3611 Env env(*this, all);
3612
3613 Account const lender{"lender"};
3614 Account const borrower{"borrower"};
3615
3616 BrokerParameters const brokerParams;
3617 env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower);
3618 env.close();
3619
3620 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3621
3622 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3623
3624 using namespace loan;
3625
3626 auto const loanSetFee = fee(env.current()->fees().base * 2);
3627 Number const principalRequest{1, 3};
3628
3629 auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0);
3630
3631 Json::Value randomData{Json::objectValue};
3632 randomData[jss::SigningPubKey] = Json::StaticString{"2600"};
3633 Json::Value sigObject{Json::objectValue};
3634 sigObject[jss::SigningPubKey] = strHex(lender.pk().slice());
3635 Serializer ss;
3637 parse(randomData).addWithoutSigningFields(ss);
3638 auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice());
3639 sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
3640
3641 forgedLoanSet[Json::StaticString{"CounterpartySignature"}] = sigObject;
3642
3643 // ? Fails because the lender hasn't signed the tx
3644 env(env.json(forgedLoanSet, fee(loanSetFee)), ter(telENV_RPC_FAILED));
3645
3646 auto const seq = env.seq(borrower);
3647 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3648 // ! Should fail because the lender hasn't signed the tx
3649 env(batch::outer(borrower, seq, batchFee, tfAllOrNothing),
3650 batch::inner(forgedLoanSet, seq + 1),
3651 batch::inner(pay(borrower, lender, XRP(1)), seq + 2),
3652 ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH));
3653 env.close();
3654
3655 // ? Check that the loan was NOT created
3656 {
3658 params[jss::account] = borrower.human();
3659 params[jss::type] = "Loan";
3660 auto const res = env.rpc("json", "account_objects", to_string(params));
3661 auto const objects = res[jss::result][jss::account_objects];
3662 BEAST_EXPECT(objects.size() == 0);
3663 }
3664 }
3665
3666 void
3668 {
3669 // From FIND-003
3670 testcase << "Wrong Max Debt Behavior";
3671
3672 using namespace jtx;
3673 using namespace std::chrono_literals;
3674 Env env(*this, all);
3675
3676 Account const issuer{"issuer"};
3677 Account const lender{"lender"};
3678
3679 BrokerParameters const brokerParams{.debtMax = 0};
3680 env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender));
3681 env.close();
3682
3683 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3684
3685 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3686
3687 if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
3688 BEAST_EXPECT(brokerSle))
3689 {
3690 BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0);
3691 }
3692
3693 using namespace loan;
3694
3695 auto const loanSetFee = fee(env.current()->fees().base * 2);
3696 Number const principalRequest{1, 3};
3697
3698 auto createJson = env.json(set(lender, broker.brokerID, principalRequest), fee(loanSetFee));
3699
3700 Json::Value counterpartyJson{Json::objectValue};
3701 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
3702 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
3703 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
3704 counterpartyJson[sfSigners] = createJson[sfSigners];
3705
3706 createJson = env.json(createJson, json(sfCounterpartySignature, counterpartyJson));
3707 env(createJson);
3708
3709 env.close();
3710 }
3711
3712 void
3714 {
3715 // From FIND-012
3716 testcase << "LoanPay xrpl::detail::computePeriodicPayment : "
3717 "valid rate";
3718
3719 using namespace jtx;
3720 using namespace std::chrono_literals;
3721 Env env(*this, all);
3722
3723 Account const issuer{"issuer"};
3724 Account const lender{"lender"};
3725 Account const borrower{"borrower"};
3726
3727 BrokerParameters const brokerParams;
3728 env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower);
3729 env.close();
3730
3731 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3732 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3733
3734 using namespace loan;
3735
3736 auto const loanSetFee = fee(env.current()->fees().base * 2);
3737 Number const principalRequest{640562, -5};
3738
3739 Number const serviceFee{2462611968};
3740 std::uint32_t const numPayments{4294967295 / 800};
3741
3742 auto createJson = env.json(
3743 set(borrower, broker.brokerID, principalRequest),
3744 fee(loanSetFee),
3745 loanServiceFee(serviceFee),
3746 paymentTotal(numPayments),
3747 json(sfCounterpartySignature, Json::objectValue));
3748
3749 createJson["CloseInterestRate"] = 55374;
3750 createJson["ClosePaymentFee"] = "3825205248";
3751 createJson["LatePaymentFee"] = "237";
3752 createJson["LoanOriginationFee"] = "0";
3753 createJson["OverpaymentFee"] = 35167;
3754 createJson["OverpaymentInterestRate"] = 1360;
3755 createJson["PaymentInterval"] = 727;
3756
3757 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
3758 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
3759 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
3760
3761 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
3762 // Fails in preclaim because principal requested can't be
3763 // represented as XRP
3764 env(createJson, ter(tecPRECISION_LOSS));
3765 env.close();
3766
3767 BEAST_EXPECT(!env.le(keylet));
3768
3769 Number const actualPrincipal{6};
3770
3771 createJson[sfPrincipalRequested] = actualPrincipal;
3772 createJson.removeMember(sfSequence.jsonName);
3773 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
3774 // Fails in doApply because the payment is too small to be
3775 // represented as XRP.
3776 env(createJson, ter(tecPRECISION_LOSS));
3777 env.close();
3778 }
3779
3780 void
3782 {
3783 // This will expand as more test cases are added. Some functionality
3784 // is tested in other test functions.
3785 testcase("RPC");
3786
3787 using namespace jtx;
3788
3789 Env env(*this, all);
3790
3791 auto lowerFee = [&]() {
3792 // Run the local fee back down.
3793 while (env.app().getFeeTrack().lowerLocalFee())
3794 ;
3795 };
3796
3797 auto const baseFee = env.current()->fees().base;
3798
3799 Account const alice{"alice"};
3800 std::string const borrowerPass = "borrower";
3801 Account const borrower{borrowerPass, KeyType::ed25519};
3802 auto const lenderPass = "lender";
3803 Account const lender{lenderPass, KeyType::ed25519};
3804
3805 env.fund(XRP(1'000'000), alice, lender, borrower);
3806 env.close();
3807 env(noop(lender));
3808 env(noop(lender));
3809 env(noop(lender));
3810 env(noop(lender));
3811 env(noop(lender));
3812 env.close();
3813
3814 {
3815 testcase("RPC AccountSet");
3817 txJson[sfTransactionType] = "AccountSet";
3818 txJson[sfAccount] = borrower.human();
3819
3820 auto const signParams = [&]() {
3821 Json::Value signParams{Json::objectValue};
3822 signParams[jss::passphrase] = borrowerPass;
3823 signParams[jss::key_type] = "ed25519";
3824 signParams[jss::tx_json] = txJson;
3825 return signParams;
3826 }();
3827 auto const jSign = env.rpc("json", "sign", to_string(signParams));
3828 BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json));
3829 auto txSignResult = jSign[jss::result][jss::tx_json];
3830 auto txSignBlob = jSign[jss::result][jss::tx_blob].asString();
3831 txSignResult.removeMember(jss::hash);
3832
3833 auto const jtx = env.jt(txJson, sig(borrower));
3834 BEAST_EXPECT(txSignResult == jtx.jv);
3835
3836 lowerFee();
3837 auto const jSubmit = env.rpc("submit", txSignBlob);
3838 BEAST_EXPECT(
3839 jSubmit.isMember(jss::result) &&
3840 jSubmit[jss::result].isMember(jss::engine_result) &&
3841 jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS");
3842
3843 lowerFee();
3844 env(jtx.jv, sig(none), seq(none), fee(none), ter(tefPAST_SEQ));
3845 }
3846
3847 {
3848 testcase("RPC LoanSet - illegal signature_target");
3849
3851 txJson[sfTransactionType] = "AccountSet";
3852 txJson[sfAccount] = borrower.human();
3853
3854 auto const borrowerSignParams = [&]() {
3856 params[jss::passphrase] = borrowerPass;
3857 params[jss::key_type] = "ed25519";
3858 params[jss::signature_target] = "Destination";
3859 params[jss::tx_json] = txJson;
3860 return params;
3861 }();
3862 auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
3863 BEAST_EXPECT(
3864 jSignBorrower.isMember(jss::result) &&
3865 jSignBorrower[jss::result].isMember(jss::error) &&
3866 jSignBorrower[jss::result][jss::error] == "invalidParams" &&
3867 jSignBorrower[jss::result].isMember(jss::error_message) &&
3868 jSignBorrower[jss::result][jss::error_message] == "Destination");
3869 }
3870 {
3871 testcase("RPC LoanSet - sign and submit borrower initiated");
3872 // 1. Borrower creates the transaction
3874 txJson[sfTransactionType] = "LoanSet";
3875 txJson[sfAccount] = borrower.human();
3876 txJson[sfCounterparty] = lender.human();
3877 txJson[sfLoanBrokerID] =
3878 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
3879 "F83F"
3880 "5C";
3881 txJson[sfPrincipalRequested] = "100000000";
3882 txJson[sfPaymentTotal] = 10000;
3883 txJson[sfPaymentInterval] = 3600;
3884 txJson[sfGracePeriod] = 300;
3885 txJson[sfFlags] = 65536; // tfLoanOverpayment
3886 txJson[sfFee] = to_string(24 * baseFee / 10);
3887
3888 // 2. Borrower signs the transaction
3889 auto const borrowerSignParams = [&]() {
3891 params[jss::passphrase] = borrowerPass;
3892 params[jss::key_type] = "ed25519";
3893 params[jss::tx_json] = txJson;
3894 return params;
3895 }();
3896 auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
3897 BEAST_EXPECTS(
3898 jSignBorrower.isMember(jss::result) &&
3899 jSignBorrower[jss::result].isMember(jss::tx_json),
3900 to_string(jSignBorrower));
3901 auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json];
3902 auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString();
3903
3904 // 2a. Borrower attempts to submit the transaction. It doesn't
3905 // work
3906 {
3907 lowerFee();
3908 auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob);
3909 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
3910 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
3911 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
3912 // Transaction fails because the CounterpartySignature is
3913 // missing
3914 BEAST_EXPECT(
3915 jSubmitBlobResult.isMember(jss::engine_result) &&
3916 jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER");
3917 }
3918
3919 // 3. Borrower sends the signed transaction to the lender
3920 // 4. Lender signs the transaction
3921 auto const lenderSignParams = [&]() {
3923 params[jss::passphrase] = lenderPass;
3924 params[jss::key_type] = "ed25519";
3925 params[jss::signature_target] = "CounterpartySignature";
3926 params[jss::tx_json] = txBorrowerSignResult;
3927 return params;
3928 }();
3929 auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams));
3930 BEAST_EXPECT(
3931 jSignLender.isMember(jss::result) &&
3932 jSignLender[jss::result].isMember(jss::tx_json));
3933 auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json];
3934 auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString();
3935
3936 // 5. Lender submits the signed transaction blob
3937 lowerFee();
3938 auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob);
3939 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
3940 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
3941 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
3942 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
3943 // To get far enough to return tecNO_ENTRY means that the
3944 // signatures all validated. Of course the transaction won't
3945 // succeed because no Vault or Broker were created.
3946 BEAST_EXPECTS(
3947 jSubmitBlobResult.isMember(jss::engine_result) &&
3948 jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY",
3949 to_string(jSubmitBlobResult));
3950
3951 BEAST_EXPECT(
3952 !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error));
3953
3954 // 4-alt. Lender submits the transaction json originally
3955 // received from the Borrower. It gets signed, but is now a
3956 // duplicate, so fails. Borrower could done this instead of
3957 // steps 4 and 5.
3958 lowerFee();
3959 auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams));
3960 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
3961 auto const jSubmitJsonResult = jSubmitJson[jss::result];
3962 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
3963 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
3964 // Since the previous tx claimed a fee, this duplicate is not
3965 // going anywhere
3966 BEAST_EXPECTS(
3967 jSubmitJsonResult.isMember(jss::engine_result) &&
3968 jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ",
3969 to_string(jSubmitJsonResult));
3970
3971 BEAST_EXPECT(
3972 !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error));
3973
3974 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
3975 }
3976
3977 {
3978 testcase("RPC LoanSet - sign and submit lender initiated");
3979 // 1. Lender creates the transaction
3981 txJson[sfTransactionType] = "LoanSet";
3982 txJson[sfAccount] = lender.human();
3983 txJson[sfCounterparty] = borrower.human();
3984 txJson[sfLoanBrokerID] =
3985 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
3986 "F83F"
3987 "5C";
3988 txJson[sfPrincipalRequested] = "100000000";
3989 txJson[sfPaymentTotal] = 10000;
3990 txJson[sfPaymentInterval] = 3600;
3991 txJson[sfGracePeriod] = 300;
3992 txJson[sfFlags] = 65536; // tfLoanOverpayment
3993 txJson[sfFee] = to_string(24 * baseFee / 10);
3994
3995 // 2. Lender signs the transaction
3996 auto const lenderSignParams = [&]() {
3998 params[jss::passphrase] = lenderPass;
3999 params[jss::key_type] = "ed25519";
4000 params[jss::tx_json] = txJson;
4001 return params;
4002 }();
4003 auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams));
4004 BEAST_EXPECT(
4005 jSignLender.isMember(jss::result) &&
4006 jSignLender[jss::result].isMember(jss::tx_json));
4007 auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json];
4008 auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString();
4009
4010 // 2a. Lender attempts to submit the transaction. It doesn't
4011 // work
4012 {
4013 lowerFee();
4014 auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob);
4015 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4016 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4017 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4018 // Transaction fails because the CounterpartySignature is
4019 // missing
4020 BEAST_EXPECT(
4021 jSubmitBlobResult.isMember(jss::engine_result) &&
4022 jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER");
4023 }
4024
4025 // 3. Lender sends the signed transaction to the Borrower
4026 // 4. Borrower signs the transaction
4027 auto const borrowerSignParams = [&]() {
4029 params[jss::passphrase] = borrowerPass;
4030 params[jss::key_type] = "ed25519";
4031 params[jss::signature_target] = "CounterpartySignature";
4032 params[jss::tx_json] = txLenderSignResult;
4033 return params;
4034 }();
4035 auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
4036 BEAST_EXPECT(
4037 jSignBorrower.isMember(jss::result) &&
4038 jSignBorrower[jss::result].isMember(jss::tx_json));
4039 auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json];
4040 auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString();
4041
4042 // 5. Borrower submits the signed transaction blob
4043 lowerFee();
4044 auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob);
4045 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4046 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4047 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4048 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
4049 // To get far enough to return tecNO_ENTRY means that the
4050 // signatures all validated. Of course the transaction won't
4051 // succeed because no Vault or Broker were created.
4052 BEAST_EXPECTS(
4053 jSubmitBlobResult.isMember(jss::engine_result) &&
4054 jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY",
4055 to_string(jSubmitBlobResult));
4056
4057 BEAST_EXPECT(
4058 !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error));
4059
4060 // 4-alt. Borrower submits the transaction json originally
4061 // received from the Lender. It gets signed, but is now a
4062 // duplicate, so fails. Lender could done this instead of steps
4063 // 4 and 5.
4064 lowerFee();
4065 auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams));
4066 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
4067 auto const jSubmitJsonResult = jSubmitJson[jss::result];
4068 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
4069 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
4070 // Since the previous tx claimed a fee, this duplicate is not
4071 // going anywhere
4072 BEAST_EXPECTS(
4073 jSubmitJsonResult.isMember(jss::engine_result) &&
4074 jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ",
4075 to_string(jSubmitJsonResult));
4076
4077 BEAST_EXPECT(
4078 !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error));
4079
4080 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
4081 }
4082 }
4083
4084 void
4086 {
4087 testcase << "Service Fee On Broker Deep Freeze";
4088 using namespace jtx;
4089 using namespace loan;
4090 Account const issuer("issuer");
4091 Account const borrower("borrower");
4092 Account const broker("broker");
4093 auto const IOU = issuer["IOU"];
4094
4095 for (bool const deepFreeze : {true, false})
4096 {
4097 Env env(*this);
4098
4099 auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) {
4100 if (auto const le = env.le(keylet::loanbroker(brokerInfo.brokerID));
4101 BEAST_EXPECT(le))
4102 {
4103 auto const account = le->at(accountField);
4104 if (auto const sleLine = env.le(keylet::line(account, IOU));
4105 BEAST_EXPECT(sleLine))
4106 {
4107 STAmount balance = sleLine->at(sfBalance);
4108 if (account > issuer.id())
4109 balance.negate();
4110 return balance;
4111 }
4112 }
4113 return STAmount{IOU};
4114 };
4115
4116 env.fund(XRP(20'000), issuer, broker, borrower);
4117 env.close();
4118
4119 env(trust(broker, IOU(20'000'000)));
4120 env(pay(issuer, broker, IOU(10'000'000)));
4121 env.close();
4122
4123 auto const brokerInfo = createVaultAndBroker(env, IOU, broker);
4124
4125 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == IOU(1'000));
4126
4127 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
4128
4129 env(set(borrower, brokerInfo.brokerID, 10'000),
4130 sig(sfCounterpartySignature, broker),
4131 loanServiceFee(IOU(100).value()),
4132 paymentInterval(100),
4133 fee(XRP(100)));
4134 env.close();
4135
4136 env(trust(borrower, IOU(20'000'000)));
4137 // The borrower increases their limit and acquires some IOU so
4138 // they can pay interest
4139 env(pay(issuer, borrower, IOU(500)));
4140 env.close();
4141
4142 if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le))
4143 {
4144 if (deepFreeze)
4145 {
4146 env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze));
4147 env.close();
4148 }
4149
4150 env(pay(borrower, keylet.key, IOU(10'100)), fee(XRP(100)));
4151 env.close();
4152
4153 if (deepFreeze)
4154 {
4155 // The fee goes to the broker pseudo-account
4156 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == IOU(1'100));
4157 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == IOU(8'999'000));
4158 }
4159 else
4160 {
4161 // The fee goes to the broker account
4162 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == IOU(8'999'100));
4163 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == IOU(1'000));
4164 }
4165 }
4166 };
4167 }
4168
4169 void
4171 {
4172 testcase << "Issuer Loan";
4173
4174 using namespace jtx;
4175 using namespace loan;
4176 Account const issuer("issuer");
4177 Account const borrower = issuer;
4178 Account const lender("lender");
4179 Env env(*this);
4180
4181 env.fund(XRP(1'000), issuer, lender);
4182
4183 std::int64_t constexpr issuerBalance = 10'000'000;
4184 MPTTester const asset(
4185 {.env = env, .issuer = issuer, .holders = {lender}, .pay = issuerBalance});
4186
4187 BrokerParameters const brokerParams{
4188 .debtMax = 200,
4189 };
4190 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
4191 auto const loanSetFee = fee(env.current()->fees().base * 2);
4192 // Create Loan
4193 env(set(borrower, broker.brokerID, 200), sig(sfCounterpartySignature, lender), loanSetFee);
4194 env.close();
4195 // Issuer should not create MPToken
4196 BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer)));
4197 // Issuer "borrowed" 200, OutstandingAmount decreased by 200
4198 BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance + 200));
4199 // Pay Loan
4200 auto const loanKeylet = keylet::loan(broker.brokerID, 1);
4201 env(pay(borrower, loanKeylet.key, asset(200)));
4202 env.close();
4203 // Issuer "re-payed" 200, OutstandingAmount increased by 200
4204 BEAST_EXPECT(env.balance(issuer, asset) == asset(-issuerBalance));
4205 }
4206
4207 void
4209 {
4210 testcase("Invalid LoanDelete");
4211 using namespace jtx;
4212 using namespace loan;
4213
4214 // preflight: temINVALID, LoanID == zero
4215 {
4216 Account const alice{"alice"};
4217 Env env(*this);
4218 env.fund(XRP(1'000), alice);
4219 env.close();
4220 env(del(alice, beast::zero), ter(temINVALID));
4221 }
4222 }
4223
4224 void
4226 {
4227 testcase("Invalid LoanManage");
4228 using namespace jtx;
4229 using namespace loan;
4230
4231 // preflight: temINVALID, LoanID == zero
4232 {
4233 Account const alice{"alice"};
4234 Env env(*this);
4235 env.fund(XRP(1'000), alice);
4236 env.close();
4237 env(manage(alice, beast::zero, tfLoanDefault), ter(temINVALID));
4238 }
4239 }
4240
4241 void
4243 {
4244 testcase("Invalid LoanPay");
4245 using namespace jtx;
4246 using namespace loan;
4247 Account const lender{"lender"};
4248 Account const issuer{"issuer"};
4249 Account const borrower{"borrower"};
4250 auto const IOU = issuer["IOU"];
4251
4252 // preclaim
4253 Env env(*this);
4254 env.fund(XRP(1'000), lender, issuer, borrower);
4255 env(trust(lender, IOU(10'000'000)));
4256 env(pay(issuer, lender, IOU(5'000'000)));
4257 BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
4258
4259 auto const loanSetFee = fee(env.current()->fees().base * 2);
4260 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
4261
4262 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4263 sig(sfCounterpartySignature, lender),
4264 loanSetFee);
4265
4266 env.close();
4267
4268 std::uint32_t const loanSequence = 1;
4269 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
4270
4271 env(fset(issuer, asfGlobalFreeze));
4272 env.close();
4273
4274 // preclaim: tecFROZEN
4275 env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecFROZEN));
4276 env.close();
4277
4278 env(fclear(issuer, asfGlobalFreeze));
4279 env.close();
4280
4281 auto const pseudoBroker = [&]() -> std::optional<Account> {
4282 if (auto brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
4283 BEAST_EXPECT(brokerSle))
4284 {
4285 return Account{"pseudo", brokerSle->at(sfAccount)};
4286 }
4287
4288 return std::nullopt;
4289 }();
4290 if (!pseudoBroker)
4291 return;
4292
4293 // Lender and pseudoaccount must both be frozen
4294 env(trust(issuer, lender["IOU"](1'000), lender, tfSetFreeze | tfSetDeepFreeze));
4295 env(trust(
4296 issuer, (*pseudoBroker)["IOU"](1'000), *pseudoBroker, tfSetFreeze | tfSetDeepFreeze));
4297 env.close();
4298
4299 // preclaim: tecFROZEN due to deep frozen
4300 env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecFROZEN));
4301 env.close();
4302
4303 // Only one needs to be unfrozen
4304 env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze));
4305 env.close();
4306
4307 // The payment is late by this point
4308 env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecEXPIRED));
4309 env.close();
4310 env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment));
4311 env.close();
4312
4313 // preclaim: tecKILLED
4314 // note that tecKILLED in loanMakePayment()
4315 // doesn't happen because of the preclaim check.
4316 env(pay(borrower, loanKeylet.key, debtMaximumRequest), ter(tecKILLED));
4317 }
4318
4319 void
4321 {
4322 testcase("Invalid LoanSet");
4323 using namespace jtx;
4324 using namespace loan;
4325 Account const lender{"lender"};
4326 Account const issuer{"issuer"};
4327 Account const borrower{"borrower"};
4328 auto const IOU = issuer["IOU"];
4329
4330 auto testWrapper = [&](auto&& test) {
4331 Env env(*this);
4332 env.fund(XRP(1'000), lender, issuer, borrower);
4333 env(trust(lender, IOU(10'000'000)));
4334 env(pay(issuer, lender, IOU(5'000'000)));
4335 BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
4336
4337 auto const loanSetFee = fee(env.current()->fees().base * 2);
4338 Number const debtMaximumRequest = brokerInfo.asset(1'000).value();
4339 test(env, brokerInfo, loanSetFee, debtMaximumRequest);
4340 };
4341
4342 // preflight:
4343 testWrapper([&](Env& env,
4344 BrokerInfo const& brokerInfo,
4345 jtx::fee const& loanSetFee,
4346 Number const& debtMaximumRequest) {
4347 // first temBAD_SIGNER: TODO
4348 // invalid grace period
4349 {
4350 // zero grace period
4351 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4352 sig(sfCounterpartySignature, lender),
4353 gracePeriod(0),
4354 loanSetFee,
4355 ter(temINVALID));
4356
4357 // grace period less than default minimum
4358 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4359 sig(sfCounterpartySignature, lender),
4360 gracePeriod(LoanSet::defaultGracePeriod - 1),
4361 loanSetFee,
4362 ter(temINVALID));
4363
4364 // grace period greater than payment interval
4365 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4366 sig(sfCounterpartySignature, lender),
4367 paymentInterval(120),
4368 gracePeriod(121),
4369 loanSetFee,
4370 ter(temINVALID));
4371 }
4372 // empty/zero broker ID
4373 {
4374 auto jv = set(borrower, uint256{}, debtMaximumRequest);
4375
4376 auto testZeroBrokerID = [&](std::string const& id, std::uint32_t flags = 0) {
4377 // empty broker ID
4378 jv[sfLoanBrokerID] = id;
4379 env(jv,
4380 sig(sfCounterpartySignature, lender),
4381 loanSetFee,
4382 txflags(flags),
4383 ter(temINVALID));
4384 };
4385 // empty broker ID
4386 testZeroBrokerID(std::string(""));
4387 // zero broker ID
4388 // needs a flag to distinguish the parsed STTx from the prior
4389 // test
4390 testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig);
4391 }
4392
4393 // preflightCheckSigningKey() failure:
4394 // can it happen? the signature is checked before transactor
4395 // executes
4396
4397 JTx const tx = env.jt(
4398 set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4399 sig(sfCounterpartySignature, lender),
4400 loanSetFee);
4401 STTx local = *(tx.stx);
4402 auto counterpartySig = local.getFieldObject(sfCounterpartySignature);
4403 auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey);
4404 badPubKey[20] ^= 0xAA;
4405 counterpartySig.setFieldVL(sfSigningPubKey, badPubKey);
4406 local.setFieldObject(sfCounterpartySignature, counterpartySig);
4407 Json::Value jvResult;
4408 jvResult[jss::tx_blob] = strHex(local.getSerializer().slice());
4409 auto res = env.rpc("json", "submit", to_string(jvResult))["result"];
4410 BEAST_EXPECT(
4411 res[jss::error] == "invalidTransaction" &&
4412 res[jss::error_exception] ==
4413 "fails local checks: Counterparty: Invalid signature.");
4414 });
4415
4416 // preclaim:
4417 testWrapper([&](Env& env,
4418 BrokerInfo const& brokerInfo,
4419 jtx::fee const& loanSetFee,
4420 Number const& debtMaximumRequest) {
4421 // canAddHoldingFailure (IOU only, if MPT doesn't have
4422 // MPTCanTransfer set, then can't create Vault/LoanBroker,
4423 // and LoanSet will fail with different error
4424 env(fclear(issuer, asfDefaultRipple));
4425 env.close();
4426 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4427 sig(sfCounterpartySignature, lender),
4428 loanSetFee,
4429 ter(terNO_RIPPLE));
4430 });
4431
4432 // doApply:
4433 testWrapper([&](Env& env,
4434 BrokerInfo const& brokerInfo,
4435 jtx::fee const& loanSetFee,
4436 Number const& debtMaximumRequest) {
4437 auto const amt = env.balance(borrower) -
4438 env.current()->fees().accountReserve(env.ownerCount(borrower));
4439 env(pay(borrower, issuer, amt));
4440
4441 // tecINSUFFICIENT_RESERVE
4442 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4443 sig(sfCounterpartySignature, lender),
4444 loanSetFee,
4446
4447 // addEmptyHolding failure
4448 env(pay(issuer, borrower, amt));
4449 env(fset(issuer, asfGlobalFreeze));
4450 env.close();
4451
4452 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4453 sig(sfCounterpartySignature, lender),
4454 loanSetFee,
4455 ter(tecFROZEN));
4456 });
4457 }
4458
4459 void
4461 {
4462 // (From FIND-006)
4463 testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount "
4464 "and MPT";
4465
4466 using namespace jtx;
4467 using namespace std::chrono_literals;
4468 Env env(*this, all);
4469
4470 Account const issuer{"issuer"};
4471 Account const lender{"lender"};
4472 Account const borrower{"borrower"};
4473
4474 env.fund(XRP(1'000'000), issuer, lender, borrower);
4475 env.close();
4476
4477 MPTTester mptt{env, issuer, mptInitNoFund};
4478 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
4479 PrettyAsset const mptAsset = mptt.issuanceID();
4480 mptt.authorize({.account = lender});
4481 mptt.authorize({.account = borrower});
4482 env(pay(issuer, lender, mptAsset(2'000'000)));
4483 env(pay(issuer, borrower, mptAsset(1'000)));
4484 env.close();
4485
4486 BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)};
4487
4488 using namespace loan;
4489
4490 auto const loanSetFee = fee(env.current()->fees().base * 2);
4491 Number const principalRequest{1, 3};
4492
4493 auto createJson = env.json(
4494 set(borrower, broker.brokerID, principalRequest),
4495 fee(loanSetFee),
4496 json(sfCounterpartySignature, Json::objectValue));
4497
4498 createJson["CloseInterestRate"] = 76671;
4499 createJson["ClosePaymentFee"] = "2061925410";
4500 createJson["GracePeriod"] = 434;
4501 createJson["InterestRate"] = 50302;
4502 createJson["LateInterestRate"] = 30322;
4503 createJson["LatePaymentFee"] = "294427911";
4504 createJson["LoanOriginationFee"] = "3250635102";
4505 createJson["LoanServiceFee"] = "9557386";
4506 createJson["OverpaymentFee"] = 51249;
4507 createJson["OverpaymentInterestRate"] = 14304;
4508 createJson["PaymentInterval"] = 434;
4509 createJson["PaymentTotal"] = "2891743748";
4510 createJson["PrincipalRequested"] = "8516.98";
4511
4512 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
4513
4514 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
4515 env(createJson, ter(temINVALID));
4516 env.close();
4517 }
4518
4519 void
4521 {
4522 // From FIND-007
4523 testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease "
4524 "rounding good";
4525
4526 using namespace jtx;
4527 using namespace std::chrono_literals;
4528 using namespace Lending;
4529 Env env(*this, all);
4530
4531 Account const issuer{"issuer"};
4532 Account const lender{"lender"};
4533 Account const borrower{"borrower"};
4534
4535 env.fund(XRP(1'000'000), issuer, lender, borrower);
4536 env.close();
4537
4538 PrettyAsset const iouAsset = issuer[iouCurrency];
4539 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4540 env(trustLenderTx);
4541 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4542 env(trustBorrowerTx);
4543 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4544 env(payLenderTx);
4545 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4546 env(payIssuerTx);
4547 env.close();
4548
4549 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
4550
4551 using namespace loan;
4552
4553 auto const baseFee = env.current()->fees().base;
4554 auto const loanSetFee = fee(baseFee * 2);
4555 Number const principalRequest{1, 3};
4556
4557 auto createJson = env.json(
4558 set(borrower, broker.brokerID, principalRequest),
4559 fee(loanSetFee),
4560 json(sfCounterpartySignature, Json::objectValue));
4561
4562 createJson["ClosePaymentFee"] = "0";
4563 createJson["GracePeriod"] = 60;
4564 createJson["InterestRate"] = 24346;
4565 createJson["LateInterestRate"] = 65535;
4566 createJson["LatePaymentFee"] = "0";
4567 createJson["LoanOriginationFee"] = "218";
4568 createJson["LoanServiceFee"] = "0";
4569 createJson["PaymentInterval"] = 60;
4570 createJson["PaymentTotal"] = 5678;
4571 createJson["PrincipalRequested"] = "9924.81";
4572
4573 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
4574 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4575 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4576
4577 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
4578 env(createJson, ter(tesSUCCESS));
4579 env.close();
4580
4581 auto const pseudoAcct = [&]() {
4582 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
4583 if (!BEAST_EXPECT(brokerSle))
4584 return Account{lender};
4585 auto const brokerPseudo = brokerSle->at(sfAccount);
4586 return Account("Broker pseudo-account", brokerPseudo);
4587 }();
4588
4589 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet);
4590 auto const originalState = getCurrentState(env, broker, keylet);
4591 verifyLoanStatus(originalState);
4592
4593 Number const payment{3'269'349'176'470'588, -12};
4594 XRPAmount const payFee{
4595 baseFee *
4596 ((payment / originalState.periodicPayment) / loanPaymentsPerFeeIncrement + 1)};
4597 auto loanPayTx =
4598 env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), fee(payFee));
4599 BEAST_EXPECT(to_string(payment) == "3269.349176470588");
4600 env(loanPayTx, ter(tesSUCCESS));
4601 env.close();
4602
4603 auto const newState = getCurrentState(env, broker, keylet);
4604 BEAST_EXPECT(
4605 isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale));
4606 BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding);
4607 BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale));
4608 BEAST_EXPECT(
4609 isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale));
4610 }
4611
4612 void
4614 {
4615 // From FIND-010
4616 testcase << "xrpl::loanComputePaymentParts : valid total interest";
4617
4618 using namespace jtx;
4619 using namespace std::chrono_literals;
4620 Env env(*this, all);
4621
4622 Account const issuer{"issuer"};
4623 Account const lender{"lender"};
4624 Account const borrower{"borrower"};
4625
4626 env.fund(XRP(1'000'000), issuer, lender, borrower);
4627 env.close();
4628
4629 PrettyAsset const iouAsset = issuer[iouCurrency];
4630 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4631 env(trustLenderTx);
4632 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4633 env(trustBorrowerTx);
4634 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4635 env(payLenderTx);
4636 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4637 env(payIssuerTx);
4638 env.close();
4639
4640 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4641
4642 using namespace loan;
4643
4644 auto const loanSetFee = fee(env.current()->fees().base * 2);
4645 Number const principalRequest{1, 3};
4646
4647 auto createJson = env.json(
4648 set(borrower, broker.brokerID, principalRequest),
4649 fee(loanSetFee),
4650 json(sfCounterpartySignature, Json::objectValue));
4651
4652 createJson["CloseInterestRate"] = 47299;
4653 createJson["ClosePaymentFee"] = "3985819770";
4654 createJson["InterestRate"] = 92;
4655 createJson["LatePaymentFee"] = "3866894865";
4656 createJson["LoanOriginationFee"] = "0";
4657 createJson["LoanServiceFee"] = "2348810240";
4658 createJson["OverpaymentFee"] = 58545;
4659 createJson["PaymentInterval"] = 60;
4660 createJson["PaymentTotal"] = 1;
4661 createJson["PrincipalRequested"] = "0.000763058";
4662
4663 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
4664 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4665 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4666
4667 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
4668 env(createJson);
4669 env.close();
4670
4671 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4672 loanPayTx["Amount"]["value"] = "0.000281284125490196";
4673 env(loanPayTx, ter(tecINSUFFICIENT_PAYMENT));
4674 env.close();
4675 }
4676
4677 void
4679 {
4680 // From FIND-005
4681 testcase << "DoS LoanPay";
4682
4683 using namespace jtx;
4684 using namespace std::chrono_literals;
4685 using namespace Lending;
4686 Env env(*this, all);
4687
4688 Account const issuer{"issuer"};
4689 Account const lender{"lender"};
4690 Account const borrower{"borrower"};
4691
4692 env.fund(XRP(1'000'000), issuer, lender, borrower);
4693 env.close();
4694
4695 PrettyAsset const iouAsset = issuer[iouCurrency];
4696 env(trust(lender, iouAsset(100'000'000)));
4697 env(trust(borrower, iouAsset(100'000'000)));
4698 env(pay(issuer, lender, iouAsset(10'000'000)));
4699 env(pay(issuer, borrower, iouAsset(1'000)));
4700 env.close();
4701
4702 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4703
4704 using namespace loan;
4705
4706 auto const loanSetFee = fee(env.current()->fees().base * 2);
4707 Number const principalRequest{1, 3};
4708 auto const baseFee = env.current()->fees().base;
4709
4710 auto createJson = env.json(
4711 set(borrower, broker.brokerID, principalRequest),
4712 fee(loanSetFee),
4713 json(sfCounterpartySignature, Json::objectValue));
4714
4715 createJson["ClosePaymentFee"] = "0";
4716 createJson["GracePeriod"] = 60;
4717 createJson["InterestRate"] = 20930;
4718 createJson["LateInterestRate"] = 77049;
4719 createJson["LatePaymentFee"] = "0";
4720 createJson["LoanServiceFee"] = "0";
4721 createJson["OverpaymentFee"] = 7;
4722 createJson["OverpaymentInterestRate"] = 66653;
4723 createJson["PaymentInterval"] = 60;
4724 createJson["PaymentTotal"] = 3239184;
4725 createJson["PrincipalRequested"] = "3959.37";
4726
4727 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
4728 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4729 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4730
4731 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
4732 env(createJson, ter(tesSUCCESS));
4733 env.close();
4734
4735 auto const stateBefore = getCurrentState(env, broker, keylet);
4736 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
4737 BEAST_EXPECT(stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction);
4738
4739 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4740 Number const amount{395937, -2};
4741 loanPayTx["Amount"]["value"] = to_string(amount);
4742 XRPAmount const payFee{
4743 baseFee *
4744 std::int64_t(amount / stateBefore.periodicPayment / loanPaymentsPerFeeIncrement + 1)};
4745 env(loanPayTx, ter(tesSUCCESS), fee(payFee));
4746 env.close();
4747
4748 auto const stateAfter = getCurrentState(env, broker, keylet);
4749 BEAST_EXPECT(
4750 stateAfter.paymentRemaining ==
4751 stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction);
4752 }
4753
4754 void
4756 {
4757 // From FIND-009
4758 testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid "
4759 "rounded";
4760
4761 using namespace jtx;
4762 using namespace std::chrono_literals;
4763 using namespace Lending;
4764 Env env(*this, all);
4765
4766 Account const issuer{"issuer"};
4767 Account const lender{"lender"};
4768 Account const borrower{"borrower"};
4769
4770 env.fund(XRP(1'000'000), issuer, lender, borrower);
4771 env.close();
4772
4773 PrettyAsset const iouAsset = issuer[iouCurrency];
4774 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4775 env(trustLenderTx);
4776 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4777 env(trustBorrowerTx);
4778 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4779 env(payLenderTx);
4780 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4781 env(payIssuerTx);
4782 env.close();
4783
4784 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4785
4786 using namespace loan;
4787
4788 auto const loanSetFee = fee(env.current()->fees().base * 2);
4789 Number const principalRequest{1, 3};
4790
4791 auto createJson = env.json(
4792 set(borrower, broker.brokerID, principalRequest),
4793 fee(loanSetFee),
4794 json(sfCounterpartySignature, Json::objectValue));
4795
4796 createJson["ClosePaymentFee"] = "0";
4797 createJson["InterestRate"] = 24346;
4798 createJson["LateInterestRate"] = 65535;
4799 createJson["LatePaymentFee"] = "0";
4800 createJson["LoanOriginationFee"] = "218";
4801 createJson["LoanServiceFee"] = "0";
4802 createJson["PaymentInterval"] = 60;
4803 createJson["PaymentTotal"] = 5678;
4804 createJson["PrincipalRequested"] = "9924.81";
4805
4806 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
4807 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4808 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4809
4810 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
4811 env(createJson, ter(tesSUCCESS));
4812 env.close();
4813
4814 auto const baseFee = env.current()->fees().base;
4815
4816 auto const stateBefore = getCurrentState(env, broker, keylet);
4817
4818 {
4819 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4820 Number const amount{3074'745'058'823'529, -12};
4821 BEAST_EXPECT(to_string(amount) == "3074.745058823529");
4822 XRPAmount const payFee{
4823 baseFee * (amount / stateBefore.periodicPayment / loanPaymentsPerFeeIncrement + 1)};
4824 loanPayTx["Amount"]["value"] = to_string(amount);
4825 env(loanPayTx, fee(payFee), ter(tesSUCCESS));
4826 env.close();
4827 }
4828
4829 {
4830 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4831 Number const amount{6732'118'170'944'051, -12};
4832 BEAST_EXPECT(to_string(amount) == "6732.118170944051");
4833 XRPAmount const payFee{
4834 baseFee * (amount / stateBefore.periodicPayment / loanPaymentsPerFeeIncrement + 1)};
4835 loanPayTx["Amount"]["value"] = to_string(amount);
4836 env(loanPayTx, fee(payFee), ter(tesSUCCESS));
4837 env.close();
4838 }
4839
4840 auto const stateAfter = getCurrentState(env, broker, keylet);
4841 // Total interest outstanding is non-negative
4842 BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding);
4843 // Principal paid is non-negative
4844 BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding);
4845 // Total value change is non-negative
4846 BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue);
4847 // Value delta is larger or same as principal delta (meaning
4848 // non-negative interest paid)
4849 BEAST_EXPECT(
4850 (stateBefore.totalValue - stateAfter.totalValue) >=
4851 (stateBefore.principalOutstanding - stateAfter.principalOutstanding));
4852 }
4853
4854 void
4856 {
4857 // From FIND-008
4858 testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded";
4859
4860 using namespace jtx;
4861 using namespace std::chrono_literals;
4862 using namespace Lending;
4863 Env env(*this, all);
4864
4865 Account const issuer{"issuer"};
4866 Account const lender{"lender"};
4867 Account const borrower{"borrower"};
4868
4869 env.fund(XRP(1'000'000), issuer, lender, borrower);
4870 env.close();
4871
4872 PrettyAsset const iouAsset = issuer[iouCurrency];
4873 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4874 env(trustLenderTx);
4875 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4876 env(trustBorrowerTx);
4877 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4878 env(payLenderTx);
4879 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
4880 env(payIssuerTx);
4881 env.close();
4882
4883 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4884 {
4885 auto const coverDepositValue = broker.asset(broker.params.coverDeposit * 10).value();
4886 env(loanBroker::coverDeposit(lender, broker.brokerID, coverDepositValue));
4887 env.close();
4888 }
4889
4890 using namespace loan;
4891
4892 auto const loanSetFee = fee(env.current()->fees().base * 2);
4893 Number const principalRequest{1, 3};
4894
4895 auto createJson = env.json(
4896 set(borrower, broker.brokerID, principalRequest),
4897 fee(loanSetFee),
4898 json(sfCounterpartySignature, Json::objectValue));
4899
4900 createJson["ClosePaymentFee"] = "0";
4901 createJson["InterestRate"] = 12833;
4902 createJson["LateInterestRate"] = 77048;
4903 createJson["LatePaymentFee"] = "0";
4904 createJson["LoanOriginationFee"] = "218";
4905 createJson["LoanServiceFee"] = "0";
4906 createJson["PaymentInterval"] = 752;
4907 createJson["PaymentTotal"] = 5678;
4908 createJson["PrincipalRequested"] = "9924.81";
4909
4910 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
4911 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4912 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4913
4914 createJson = env.json(createJson, sig(sfCounterpartySignature, lender));
4915 env(createJson, ter(tesSUCCESS));
4916 env.close();
4917
4918 auto const baseFee = env.current()->fees().base;
4919
4920 auto const stateBefore = getCurrentState(env, broker, keylet);
4921 BEAST_EXPECT(stateBefore.paymentRemaining == 5678);
4922 BEAST_EXPECT(stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction);
4923
4924 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4925 Number const amount{9924'81, -2};
4926 BEAST_EXPECT(to_string(amount) == "9924.81");
4927 XRPAmount const payFee{
4928 baseFee * (amount / stateBefore.periodicPayment / loanPaymentsPerFeeIncrement + 1)};
4929 loanPayTx["Amount"]["value"] = to_string(amount);
4930 env(loanPayTx, fee(payFee), ter(tesSUCCESS));
4931 env.close();
4932
4933 auto const stateAfter = getCurrentState(env, broker, keylet);
4934 BEAST_EXPECT(
4935 stateAfter.paymentRemaining ==
4936 stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction);
4937 }
4938
4939 void
4941 {
4942 // For FIND-013
4943 testcase << "Prevent nextPaymentDueDate overflow";
4944
4945 using namespace jtx;
4946 using namespace std::chrono_literals;
4947 using namespace Lending;
4948 Env env(*this, all);
4949
4950 Account const issuer{"issuer"};
4951 Account const lender{"lender"};
4952 Account const borrower{"borrower"};
4953
4954 env.fund(XRP(1'000'000), issuer, lender, borrower);
4955 env.close();
4956
4957 PrettyAsset const iouAsset = issuer[iouCurrency];
4958 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4959 env(trustLenderTx);
4960 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4961 env(trustBorrowerTx);
4962 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4963 env(payLenderTx);
4964 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
4965 env(payIssuerTx);
4966 env.close();
4967
4968 BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}};
4969 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
4970
4971 using namespace loan;
4972
4973 auto const loanSetFee = fee(env.current()->fees().base * 2);
4974
4975 using timeType = decltype(sfNextPaymentDueDate)::type::value_type;
4977 timeType constexpr maxTime = std::numeric_limits<timeType>::max();
4978 static_assert(maxTime == 4'294'967'295);
4979
4980 auto const baseJson = [&]() {
4981 auto createJson = env.json(
4982 set(borrower, broker.brokerID, Number{55524'81, -2}),
4983 fee(loanSetFee),
4984 closePaymentFee(0),
4985 gracePeriod(LoanSet::defaultGracePeriod),
4986 interestRate(TenthBips32(12833)),
4987 lateInterestRate(TenthBips32(77048)),
4988 latePaymentFee(0),
4989 loanOriginationFee(218),
4990 json(sfCounterpartySignature, Json::objectValue));
4991
4992 createJson.removeMember(sfSequence.getJsonName());
4993
4994 return createJson;
4995 }();
4996
4997 auto const baseFee = env.current()->fees().base;
4998
4999 auto parentCloseTime = [&]() {
5000 return env.current()->parentCloseTime().time_since_epoch().count();
5001 };
5002 auto maxLoanTime = [&]() {
5003 auto const startDate = parentCloseTime();
5004
5005 BEAST_EXPECT(startDate >= 50);
5006
5007 return maxTime - startDate;
5008 };
5009
5010 {
5011 // straight-up overflow: interval
5012 auto const interval = maxLoanTime() + 1;
5013 auto const total = 1;
5014 auto createJson = env.json(baseJson, paymentInterval(interval), paymentTotal(total));
5015
5016 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
5017 env.close();
5018 }
5019 {
5020 // straight-up overflow: total
5021 // min interval is 60
5022 auto const interval = 60;
5023 auto const total = maxLoanTime() + 1;
5024 auto createJson = env.json(baseJson, paymentInterval(interval), paymentTotal(total));
5025
5026 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
5027 env.close();
5028 }
5029 {
5030 // straight-up overflow: grace period
5031 // min interval is 60
5032 auto const interval = maxLoanTime() + 1;
5033 auto const total = 1;
5034 auto const grace = interval;
5035 auto createJson = env.json(
5036 baseJson, paymentInterval(interval), paymentTotal(total), gracePeriod(grace));
5037
5038 // The grace period can't be larger than the interval.
5039 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
5040 env.close();
5041 }
5042 {
5043 // Overflow with multiplication of a few large intervals
5044 auto const interval = 1'000'000'000;
5045 auto const total = 10;
5046 auto createJson = env.json(baseJson, paymentInterval(interval), paymentTotal(total));
5047
5048 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
5049 env.close();
5050 }
5051 {
5052 // Overflow with multiplication of many small payments
5053 // min interval is 60
5054 auto const interval = 60;
5055 auto const total = 1'000'000'000;
5056 auto createJson = env.json(baseJson, paymentInterval(interval), paymentTotal(total));
5057
5058 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
5059 env.close();
5060 }
5061 {
5062 // Overflow with an absurdly large grace period
5063 // min interval is 60
5064 auto const total = 60;
5065 auto const interval = (maxLoanTime() - total) / total;
5066 auto const grace = interval;
5067 auto createJson = env.json(
5068 baseJson, paymentInterval(interval), paymentTotal(total), gracePeriod(grace));
5069
5070 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
5071 env.close();
5072 }
5073 {
5074 // Start date when the ledger is closed will be larger
5075 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
5076 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5077 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5078
5079 auto const grace = 100;
5080 auto const interval = maxLoanTime() - grace;
5081 auto const total = 1;
5082 auto createJson = env.json(
5083 baseJson, paymentInterval(interval), paymentTotal(total), gracePeriod(grace));
5084
5085 env(createJson, sig(sfCounterpartySignature, lender), ter(tesSUCCESS));
5086 env.close();
5087
5088 // The transaction is killed in the closed ledger
5089 auto const meta = env.meta();
5090 if (BEAST_EXPECT(meta))
5091 {
5092 BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED);
5093 }
5094
5095 // If the transaction had succeeded, the loan would exist
5096 auto const loanSle = env.le(keylet);
5097 // but it doesn't
5098 BEAST_EXPECT(!loanSle);
5099 }
5100 {
5101 // Start date when the ledger is closed will be larger
5102 auto const brokerStateBefore = env.le(keylet::loanbroker(broker.brokerID));
5103 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5104 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5105
5106 auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10;
5107 auto const grace = 5'000;
5108 auto const interval = maxTime - closeStartDate - grace;
5109 auto const total = 1;
5110 auto createJson = env.json(
5111 baseJson, paymentInterval(interval), paymentTotal(total), gracePeriod(grace));
5112
5113 env(createJson, sig(sfCounterpartySignature, lender), ter(tesSUCCESS));
5114 env.close();
5115
5116 // The transaction succeeds in the closed ledger
5117 auto const meta = env.meta();
5118 if (BEAST_EXPECT(meta))
5119 {
5120 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
5121 }
5122
5123 // This loan exists
5124 auto const afterState = getCurrentState(env, broker, keylet);
5125 BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace);
5126 BEAST_EXPECT(afterState.previousPaymentDate == 0);
5127 BEAST_EXPECT(afterState.paymentRemaining == 1);
5128 }
5129
5130 {
5131 // Ensure the borrower has funds to pay back the loan
5132 env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2})));
5133
5134 // Start date when the ledger is closed will be larger
5135 auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10;
5136 auto const grace = 5'000;
5137 auto const maxLoanTime = maxTime - closeStartDate - grace;
5138 auto const total = [&]() {
5139 if (maxLoanTime % 5 == 0)
5140 return 5;
5141 if (maxLoanTime % 3 == 0)
5142 return 3;
5143 if (maxLoanTime % 2 == 0)
5144 return 2;
5145 return 0;
5146 }();
5147 if (!BEAST_EXPECT(total != 0))
5148 return;
5149
5150 auto const brokerState = env.le(keylet::loanbroker(broker.brokerID));
5151 // Intentionally shadow the outer values
5152 auto const loanSequence = brokerState->at(sfLoanSequence);
5153 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5154
5155 auto const interval = maxLoanTime / total;
5156 auto createJson = env.json(
5157 baseJson, paymentInterval(interval), paymentTotal(total), gracePeriod(grace));
5158
5159 env(createJson, sig(sfCounterpartySignature, lender), ter(tesSUCCESS));
5160 env.close();
5161
5162 // This loan exists
5163 auto const beforeState = getCurrentState(env, broker, keylet);
5164 BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval);
5165 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
5166 BEAST_EXPECT(beforeState.paymentRemaining == total);
5167 BEAST_EXPECT(beforeState.periodicPayment > 0);
5168
5169 // pay all but the last payment
5170 {
5171 NumberRoundModeGuard const mg{Number::upward};
5172 Number const payment = beforeState.periodicPayment * (total - 1);
5173 XRPAmount const payFee{baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)};
5174 STAmount const paymentAmount =
5175 roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale);
5176 auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), fee(payFee));
5177 env(loanPayTx, ter(tesSUCCESS));
5178 env.close();
5179 }
5180
5181 // The loan is on the last payment
5182 auto const afterState = getCurrentState(env, broker, keylet);
5183 BEAST_EXPECT(afterState.paymentRemaining == 1);
5184 BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace);
5185 BEAST_EXPECT(afterState.previousPaymentDate == maxTime - grace - interval);
5186 }
5187 }
5188
5189 void
5191 {
5192 testcase("Require Auth - Implicit Pseudo-account authorization");
5193 using namespace jtx;
5194 using namespace loan;
5195 Account const lender{"lender"};
5196 Account const issuer{"issuer"};
5197 Account const borrower{"borrower"};
5198 Env env(*this);
5199
5200 env.fund(XRP(100'000), issuer, lender, borrower);
5201 env.close();
5202
5203 auto asset = MPTTester({
5204 .env = env,
5205 .issuer = issuer,
5206 .holders = {lender, borrower},
5207 .flags = MPTDEXFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock,
5208 .authHolder = true,
5209 });
5210
5211 env(pay(issuer, lender, asset(5'000'000)));
5212 BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)};
5213
5214 auto const loanSetFee = fee(env.current()->fees().base * 2);
5215 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
5216
5217 auto forUnauthAuth = [&](auto&& doTx) {
5218 for (auto const flag : {tfMPTUnauthorize, 0u})
5219 {
5220 asset.authorize({.account = issuer, .holder = borrower, .flags = flag});
5221 env.close();
5222 doTx(flag == 0);
5223 env.close();
5224 }
5225 };
5226
5227 // Can't create a loan if the borrower is not authorized
5228 forUnauthAuth([&](bool authorized) {
5229 auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
5230 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
5231 sig(sfCounterpartySignature, lender),
5232 loanSetFee,
5233 err);
5234 });
5235
5236 std::uint32_t constexpr loanSequence = 1;
5237 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
5238
5239 // Can't loan pay if the borrower is not authorized
5240 forUnauthAuth([&](bool authorized) {
5241 auto const err = !authorized ? ter(tecNO_AUTH) : ter(tesSUCCESS);
5242 env(pay(borrower, loanKeylet.key, debtMaximumRequest), err);
5243 });
5244 }
5245
5246 void
5248 {
5249 testcase("CoverDeposit and CoverWithdraw reject MPT without CanTransfer");
5250 using namespace jtx;
5251 using namespace loanBroker;
5252
5253 Env env(*this, all);
5254
5255 Account const issuer{"issuer"};
5256 Account const alice{"alice"};
5257
5258 env.fund(XRP(100'000), issuer, alice);
5259 env.close();
5260
5261 MPTTester mpt{env, issuer, mptInitNoFund};
5262
5263 mpt.create({.flags = tfMPTCanTransfer, .mutableFlags = tmfMPTCanMutateCanTransfer});
5264
5265 env.close();
5266
5267 PrettyAsset const asset = mpt["MPT"];
5268 mpt.authorize({.account = alice});
5269 env.close();
5270
5271 // Issuer can fund the holder even if CanTransfer is not set.
5272 env(pay(issuer, alice, asset(100)));
5273 env.close();
5274
5275 Vault const vault{env};
5276 auto const [createTx, vaultKeylet] = vault.create({.owner = alice, .asset = asset});
5277 env(createTx);
5278 env.close();
5279
5280 auto const brokerKeylet = keylet::loanbroker(alice.id(), env.seq(alice));
5281 env(set(alice, vaultKeylet.key));
5282 env.close();
5283
5284 auto const brokerSle = env.le(brokerKeylet);
5285 if (!BEAST_EXPECT(brokerSle))
5286 return;
5287
5288 Account const pseudoAccount{"Loan Broker pseudo-account", brokerSle->at(sfAccount)};
5289
5290 // Remove CanTransfer after the broker is set up.
5291 mpt.set({.mutableFlags = tmfMPTClearCanTransfer});
5292 env.close();
5293
5294 // Standard Payment path should forbid third-party transfers.
5295 env(pay(alice, pseudoAccount, asset(1)), ter(tecNO_AUTH));
5296 env.close();
5297
5298 // Cover cannot be transferred to broker account
5299 auto const depositAmount = asset(1);
5300 env(coverDeposit(alice, brokerKeylet.key, depositAmount), ter{tecNO_AUTH});
5301 env.close();
5302
5303 if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed))
5304 {
5305 BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0);
5306 env.require(balance(pseudoAccount, asset(0)));
5307 }
5308
5309 // Set CanTransfer again and transfer some deposit
5310 mpt.set({.mutableFlags = tmfMPTSetCanTransfer});
5311 env.close();
5312
5313 env(coverDeposit(alice, brokerKeylet.key, depositAmount));
5314 env.close();
5315
5316 if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed))
5317 {
5318 BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1);
5319 env.require(balance(pseudoAccount, depositAmount));
5320 }
5321
5322 // Remove CanTransfer after the deposit
5323 mpt.set({.mutableFlags = tmfMPTClearCanTransfer});
5324 env.close();
5325
5326 // Cover cannot be transferred from broker account
5327 env(coverWithdraw(alice, brokerKeylet.key, depositAmount), ter{tecNO_AUTH});
5328 env.close();
5329
5330 // Set CanTransfer again and withdraw
5331 mpt.set({.mutableFlags = tmfMPTSetCanTransfer});
5332 env.close();
5333
5334 env(coverWithdraw(alice, brokerKeylet.key, depositAmount));
5335 env.close();
5336
5337 if (auto const refreshed = env.le(brokerKeylet); BEAST_EXPECT(refreshed))
5338 {
5339 BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0);
5340 env.require(balance(pseudoAccount, asset(0)));
5341 }
5342 }
5343
5344#if LOANTODO
5345 void
5346 testLoanPayLateFullPaymentBypassesPenalties()
5347 {
5348 testcase("LoanPay full payment skips late penalties");
5349 using namespace jtx;
5350 using namespace loan;
5351 using namespace std::chrono_literals;
5352
5353 Env env(*this, all);
5354
5355 Account const issuer{"issuer"};
5356 Account const lender{"lender"};
5357 Account const borrower{"borrower"};
5358
5359 env.fund(XRP(1'000'000), issuer, lender, borrower);
5360 env.close();
5361
5362 PrettyAsset const asset = issuer[iouCurrency];
5363 env(trust(lender, asset(100'000'000)));
5364 env(trust(borrower, asset(100'000'000)));
5365 env(pay(issuer, lender, asset(50'000'000)));
5366 env(pay(issuer, borrower, asset(5'000'000)));
5367 env.close();
5368
5369 BrokerInfo broker{createVaultAndBroker(env, asset, lender)};
5370
5371 auto const loanSetFee = fee(env.current()->fees().base * 2);
5372
5373 auto const brokerPreLoan = env.le(keylet::loanbroker(broker.brokerID));
5374 if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
5375 return;
5376
5377 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
5378 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
5379
5380 Number const principal = asset(1'000).value();
5381 Number const serviceFee = asset(2).value();
5382 Number const lateFee = asset(5).value();
5383 Number const closeFee = asset(4).value();
5384
5385 env(set(borrower, broker.brokerID, principal),
5386 sig(sfCounterpartySignature, lender),
5387 loanServiceFee(serviceFee),
5388 latePaymentFee(lateFee),
5389 closePaymentFee(closeFee),
5393 paymentTotal(12),
5394 paymentInterval(600),
5395 gracePeriod(0),
5396 fee(loanSetFee));
5397 env.close();
5398
5399 auto state1 = getCurrentState(env, broker, loanKeylet);
5400 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
5401 return;
5402
5403 using d = NetClock::duration;
5404 using tp = NetClock::time_point;
5405 auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}};
5406 env.close(overdueClose);
5407
5408 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
5409 auto const loanSle = env.le(loanKeylet);
5410 if (!BEAST_EXPECT(brokerSle && loanSle))
5411 return;
5412
5413 auto state = getCurrentState(env, broker, loanKeylet);
5414
5415 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
5416 TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)};
5417 TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)};
5418 TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)};
5419
5420 Number const closePaymentFeeRounded =
5421 roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale);
5422 Number const latePaymentFeeRounded =
5423 roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale);
5424
5425 auto const roundedLoanState = constructLoanState(
5426 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
5427 Number const totalInterestOutstanding = roundedLoanState.interestDue;
5428
5429 auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval);
5430 auto const rawLoanState = computeTheoreticalLoanState(
5431 state.periodicPayment, periodicRate, state.paymentRemaining, managementFeeRate);
5432
5433 auto const parentCloseTime = env.current()->parentCloseTime();
5434 auto const startDateSeconds =
5435 static_cast<std::uint32_t>(state.startDate.time_since_epoch().count());
5436
5437 Number const fullPaymentInterest = computeFullPaymentInterest(
5438 rawLoanState.principalOutstanding,
5439 periodicRate,
5440 parentCloseTime,
5441 state.paymentInterval,
5442 state.previousPaymentDate,
5443 startDateSeconds,
5444 closeInterestRateValue);
5445
5446 Number const roundedFullInterestAmount =
5447 roundToAsset(broker.asset, fullPaymentInterest, state.loanScale);
5448 Number const roundedFullManagementFee = computeManagementFee(
5449 broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale);
5450 Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee;
5451
5452 Number const trackedValueDelta =
5453 state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding;
5454 Number const untrackedManagementFee =
5455 closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding;
5456 Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding;
5457
5458 Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee;
5459 BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale));
5460
5461 auto const overdueSeconds =
5462 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
5463 if (!BEAST_EXPECT(overdueSeconds > 0))
5464 return;
5465
5466 Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds);
5467 Number const lateInterestRaw = state.principalOutstanding * overdueRate;
5468 Number const lateInterestRounded =
5469 roundToAsset(broker.asset, lateInterestRaw, state.loanScale);
5470 Number const lateManagementFeeRounded = computeManagementFee(
5471 broker.asset, lateInterestRounded, managementFeeRate, state.loanScale);
5472 Number const penaltyDue =
5473 lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded;
5474 BEAST_EXPECT(penaltyDue > Number{});
5475
5476 auto const balanceBefore = env.balance(borrower, broker.asset).number();
5477
5478 STAmount const paymentAmount{broker.asset.raw(), baseFullDue};
5479 env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
5480 env.close();
5481
5482 if (auto const meta = env.meta(); BEAST_EXPECT(meta))
5483 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
5484
5485 auto const balanceAfter = env.balance(borrower, broker.asset).number();
5486 Number const actualPaid = balanceBefore - balanceAfter;
5487 BEAST_EXPECT(actualPaid == baseFullDue);
5488
5489 Number const expectedWithPenalty = baseFullDue + penaltyDue;
5490 BEAST_EXPECT(expectedWithPenalty > actualPaid);
5491 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
5492 }
5493
5494 void
5495 testLoanCoverMinimumRoundingExploit()
5496 {
5497 auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) {
5498 testcase << "LoanBrokerCoverClawback drains cover via rounding"
5499 << " principalRequested=" << to_string(principalRequest);
5500
5501 using namespace jtx;
5502 using namespace loan;
5503 using namespace loanBroker;
5504
5505 Env env(*this, all);
5506
5507 Account const issuer{"issuer"};
5508 Account const lender{"lender"};
5509 Account const borrower{"borrower"};
5510
5511 env.fund(XRP(1'000'000'000), issuer, lender, borrower);
5512 env.close();
5513
5514 env(fset(issuer, asfAllowTrustLineClawback));
5515 env.close();
5516
5517 PrettyAsset const asset = issuer[iouCurrency];
5518 env(trust(lender, asset(2'000'0000)));
5519 env(trust(borrower, asset(2'000'0000)));
5520 env.close();
5521
5522 env(pay(issuer, lender, asset(2'000'0000)));
5523 env.close();
5524
5525 BrokerParameters brokerParams{.debtMax = 0, .coverRateMin = TenthBips32{10'000}};
5526 BrokerInfo broker{createVaultAndBroker(env, asset, lender, brokerParams)};
5527
5528 auto const loanSetFee = fee(env.current()->fees().base * 2);
5529 auto createTx = env.jt(
5530 set(borrower, broker.brokerID, principalRequest),
5531 sig(sfCounterpartySignature, lender),
5532 loanSetFee,
5533 paymentInterval(600),
5534 paymentTotal(1),
5535 gracePeriod(60));
5536 env(createTx);
5537 env.close();
5538
5539 auto const brokerBefore = env.le(keylet::loanbroker(broker.brokerID));
5540 BEAST_EXPECT(brokerBefore);
5541 if (!brokerBefore)
5542 return;
5543
5544 Number const debtOutstanding = brokerBefore->at(sfDebtTotal);
5545 Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable);
5546
5547 BEAST_EXPECT(debtOutstanding > Number{});
5548 BEAST_EXPECT(coverAvailableBefore > Number{});
5549
5550 log << "debt=" << to_string(debtOutstanding)
5551 << " cover_available=" << to_string(coverAvailableBefore);
5552
5553 env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID));
5554 env.close();
5555
5556 auto const brokerAfter = env.le(keylet::loanbroker(broker.brokerID));
5557 BEAST_EXPECT(brokerAfter);
5558 if (!brokerAfter)
5559 return;
5560
5561 Number const debtAfter = brokerAfter->at(sfDebtTotal);
5562 // the debt has not changed
5563 BEAST_EXPECT(debtAfter == debtOutstanding);
5564
5565 Number const coverAvailableAfter = brokerAfter->at(sfCoverAvailable);
5566
5567 // since the cover rate min != 0, the cover available should not
5568 // be zero
5569 BEAST_EXPECT(coverAvailableAfter != Number{});
5570 };
5571
5572 // Call the lambda with different principal values
5573 testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units
5574 testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units
5575 testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units
5576 testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units
5577 }
5578#endif
5579
5580 void
5582 {
5583 // --- PoC Summary ----------------------------------------------------
5584 // Scenario: Borrower makes one periodic payment early (before next due)
5585 // so doPayment sets sfPreviousPaymentDueDate to the (future)
5586 // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one
5587 // interval. Borrower then immediately performs a full-payment
5588 // (tfLoanFullPayment). Why it matters: Full-payment interest accrual
5589 // uses
5590 // delta = now - max(prevPaymentDate, startDate)
5591 // with an unsigned clock representation (uint32). If prevPaymentDate is
5592 // in the future, the subtraction underflows to a very large positive
5593 // number. This inflates roundedFullInterest and total full-close due,
5594 // and LoanPay applies the inflated valueChange to the vault
5595 // (sfAssetsTotal), increasing NAV.
5596 // --------------------------------------------------------------------
5597 testcase("PoC: Unsigned-underflow full-pay accrual after early periodic");
5598
5599 using namespace jtx;
5600 using namespace loan;
5601 using namespace std::chrono_literals;
5602
5603 Env env(*this, all);
5604
5605 Account const lender{"poc_lender4"};
5606 Account const borrower{"poc_borrower4"};
5607 env.fund(XRP(3'000'000), lender, borrower);
5608 env.close();
5609
5610 PrettyAsset const asset{xrpIssue(), 1'000'000};
5611 BrokerParameters const brokerParams{};
5612 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
5613
5614 // Create a 3-payment loan so full-payment path is enabled after 1
5615 // periodic payment.
5616 auto const loanSetFee = fee(env.current()->fees().base * 2);
5617 Number const principalRequest = asset(1000).value();
5618 auto const originationFee = asset(0).value();
5619 auto const serviceFee = asset(1).value();
5620 auto const serviceFeePA = asset(1);
5621 auto const lateFee = asset(0).value();
5622 auto const closeFee = asset(0).value();
5623 auto const interest = percentageToTenthBips(12);
5624 auto const lateInterest = percentageToTenthBips(12) / 10;
5625 auto const closeInterest = percentageToTenthBips(12) / 10;
5626 auto const overpaymentInterest = percentageToTenthBips(12) / 10;
5627 auto const total = 3u;
5628 auto const interval = 600u;
5629 auto const grace = 60u;
5630
5631 auto createJtx = env.jt(
5632 set(borrower, broker.brokerID, principalRequest, 0),
5633 sig(sfCounterpartySignature, lender),
5634 loanOriginationFee(originationFee),
5635 loanServiceFee(serviceFee),
5636 latePaymentFee(lateFee),
5637 closePaymentFee(closeFee),
5638 overpaymentFee(percentageToTenthBips(5) / 10),
5639 interestRate(interest),
5640 lateInterestRate(lateInterest),
5641 closeInterestRate(closeInterest),
5642 overpaymentInterestRate(overpaymentInterest),
5643 paymentTotal(total),
5644 paymentInterval(interval),
5645 gracePeriod(grace),
5646 fee(loanSetFee));
5647
5648 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
5649 BEAST_EXPECT(brokerSle);
5650 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
5651 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
5652
5653 env(createJtx);
5654 env.close();
5655
5656 // Compute a regular periodic due and pay it early (before next due).
5657 auto state = getCurrentState(env, broker, loanKeylet);
5658 Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval);
5659 auto const components = detail::computePaymentComponents(
5660 asset.raw(),
5661 state.loanScale,
5662 state.totalValue,
5663 state.principalOutstanding,
5664 state.managementFeeOutstanding,
5665 state.periodicPayment,
5666 periodicRate,
5667 state.paymentRemaining,
5668 brokerParams.managementFeeRate);
5669 STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()};
5670 // now < nextDue immediately after creation, so this is an early pay.
5671 env(pay(borrower, loanKeylet.key, regularDue));
5672 env.close();
5673
5674 // Immediately attempt a full payoff. Compute the exact full-payment
5675 // due to ensure the tx applies.
5676 auto after = getCurrentState(env, broker, loanKeylet);
5677 auto const loanSle = env.le(loanKeylet);
5678 BEAST_EXPECT(loanSle);
5679 auto const brokerSle2 = env.le(keylet::loanbroker(broker.brokerID));
5680 BEAST_EXPECT(brokerSle2);
5681
5682 auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{};
5683 auto const closeInterestRate =
5684 loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{};
5685 auto const managementFeeRate =
5686 brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{};
5687
5688 Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval);
5689 // Accrued + prepayment-penalty interest based on current periodic
5690 // schedule
5691 auto const fullPaymentInterest = computeFullPaymentInterest(
5692 detail::loanPrincipalFromPeriodicPayment(
5693 after.periodicPayment, periodicRate2, after.paymentRemaining),
5694 periodicRate2,
5695 env.current()->parentCloseTime(),
5696 after.paymentInterval,
5697 after.previousPaymentDate,
5698 static_cast<std::uint32_t>(after.startDate.time_since_epoch().count()),
5699 closeInterestRate);
5700
5701 // Round to asset scale and split interest/fee parts
5702 auto const roundedInterest =
5703 roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
5704 Number const roundedFullMgmtFee =
5705 computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale);
5706 Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee;
5707
5708 // Show both signed and unsigned deltas to highlight the underflow.
5709 auto const nowSecs =
5710 static_cast<std::uint32_t>(env.current()->parentCloseTime().time_since_epoch().count());
5711 auto const startSecs =
5712 static_cast<std::uint32_t>(after.startDate.time_since_epoch().count());
5713 auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs);
5714 auto const signedDelta =
5715 static_cast<std::int64_t>(nowSecs) - static_cast<std::int64_t>(lastPaymentDate);
5716 auto const unsignedDelta = static_cast<std::uint32_t>(nowSecs - lastPaymentDate);
5717 log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs
5718 << " now=" << nowSecs << " signedDelta=" << signedDelta
5719 << " unsignedDelta=" << unsignedDelta << std::endl;
5720
5721 // Reference (clamped) computation: emulate a non-negative accrual
5722 // window by clamping prevPaymentDate to 'now' for the full-pay path.
5723 auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
5724 auto const fullPaymentInterestClamped = computeFullPaymentInterest(
5725 detail::loanPrincipalFromPeriodicPayment(
5726 after.periodicPayment, periodicRate2, after.paymentRemaining),
5727 periodicRate2,
5728 env.current()->parentCloseTime(),
5729 after.paymentInterval,
5730 prevClamped,
5731 startSecs,
5732 closeInterestRate);
5733 auto const roundedInterestClamped =
5734 roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale);
5735 Number const roundedFullMgmtFeeClamped = computeManagementFee(
5736 asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale);
5737 Number const roundedFullInterestClamped =
5738 roundedInterestClamped - roundedFullMgmtFeeClamped;
5739 STAmount const fullDueClamped{
5740 asset,
5741 after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped +
5742 closePaymentFee};
5743
5744 // Collect vault NAV before closing payment
5745 auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{};
5746 auto const vaultKey2 = keylet::vault(vaultId2);
5747 auto const vaultBefore = env.le(vaultKey2);
5748 BEAST_EXPECT(vaultBefore);
5749 Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{};
5750
5751 STAmount const fullDue{
5752 asset,
5753 after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee +
5754 closePaymentFee};
5755
5756 log << "PoC payoff: principalOutstanding=" << after.principalOutstanding
5757 << " roundedFullInterest=" << roundedFullInterest
5758 << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee
5759 << " fullDue=" << to_string(fullDue.getJson()) << std::endl;
5760 log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped
5761 << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped
5762 << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl;
5763
5764 env(pay(borrower, loanKeylet.key, fullDue), txflags(tfLoanFullPayment));
5765 env.close();
5766
5767 // Sanity: underflow present (unsigned delta very large relative to
5768 // interval)
5769 BEAST_EXPECT(unsignedDelta > after.paymentInterval);
5770
5771 // Compare vault NAV before/after the full close
5772 auto const vaultAfter = env.le(vaultKey2);
5773 BEAST_EXPECT(vaultAfter);
5774 if (vaultAfter)
5775 {
5776 auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
5777 log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore
5778 << " assetsTotalAfter=" << assetsTotalAfter
5779 << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl;
5780
5781 // Value-based proof: underflowed window yields a payoff larger than
5782 // the clamped (non-underflow) reference.
5783 BEAST_EXPECT(fullDue == fullDueClamped);
5784 if (fullDue > fullDueClamped)
5785 log << "PoC delta: overcharge (fullDue > clamped)" << std::endl;
5786 }
5787
5788 // Loan should be paid off
5789 auto const finalLoan = env.le(loanKeylet);
5790 BEAST_EXPECT(finalLoan);
5791 if (finalLoan)
5792 {
5793 BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0);
5794 BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0);
5795 }
5796 }
5797
5798 void
5800 {
5801 testcase("Dust manipulation");
5802
5803 using namespace jtx;
5804 using namespace std::chrono_literals;
5805 Env env(*this, all);
5806
5807 // Setup: Create accounts
5808 Account const issuer{"issuer"};
5809 Account const lender{"lender"};
5810 Account const borrower{"borrower"};
5811 Account const victim{"victim"};
5812
5813 env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim);
5814 env.close();
5815
5816 // Step 1: Create vault with IOU asset
5817 auto asset = issuer["USD"];
5818 env(trust(lender, asset(100000)));
5819 env(trust(borrower, asset(100000)));
5820 env(trust(victim, asset(100000)));
5821 env(pay(issuer, lender, asset(50000)));
5822 env(pay(issuer, borrower, asset(50000)));
5823 env(pay(issuer, victim, asset(50000)));
5824 env.close();
5825
5826 BrokerParameters const brokerParams{
5827 .vaultDeposit = 10000,
5828 .debtMax = Number{0},
5829 .coverRateMin = TenthBips32{1000},
5830 .coverRateLiquidation = TenthBips32{2500}};
5831
5832 auto broker = createVaultAndBroker(env, asset, lender, brokerParams);
5833
5834 auto const loanKeyletOpt = [&]() -> std::optional<Keylet> {
5835 auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID));
5836 if (!BEAST_EXPECT(brokerSle))
5837 return std::nullopt;
5838
5839 // Broker has no loans
5840 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
5841
5842 // The loan keylet is based on the LoanSequence of the
5843 // _LOAN_BROKER_ object.
5844 auto const loanSequence = brokerSle->at(sfLoanSequence);
5845 return keylet::loan(broker.brokerID, loanSequence);
5846 }();
5847 if (!loanKeyletOpt)
5848 return;
5849
5850 auto const& vaultKeylet = broker.vaultKeylet();
5851
5852 {
5853 auto const vaultSle = env.le(vaultKeylet);
5854 Number const assetsTotal = vaultSle->at(sfAssetsTotal);
5855 Number const assetsAvail = vaultSle->at(sfAssetsAvailable);
5856
5857 log << "Before loan creation:" << std::endl;
5858 log << " AssetsTotal: " << assetsTotal << std::endl;
5859 log << " AssetsAvailable: " << assetsAvail << std::endl;
5860 log << " Difference: " << (assetsTotal - assetsAvail) << std::endl;
5861
5862 // before the loan the assets total and available should be equal
5863 BEAST_EXPECT(assetsAvail == assetsTotal);
5864 BEAST_EXPECT(assetsAvail == broker.asset(brokerParams.vaultDeposit).number());
5865 }
5866
5867 Keylet const& loanKeylet = *loanKeyletOpt;
5868
5869 LoanParameters const loanParams{
5870 .account = lender,
5871 .counter = borrower,
5872 .principalRequest = Number{100},
5873 .interest = TenthBips32{1922},
5874 .payTotal = 5816,
5875 .payInterval = 86400 * 6,
5876 .gracePd = 86400 * 5,
5877 };
5878
5879 env(loanParams(env, broker));
5880 env.close();
5881
5882 // Wait for loan to be late enough to default
5883 env.close(std::chrono::seconds(86400 * 40)); // 40 days
5884
5885 {
5886 auto const vaultSle = env.le(vaultKeylet);
5887 Number const assetsTotal = vaultSle->at(sfAssetsTotal);
5888 Number const assetsAvail = vaultSle->at(sfAssetsAvailable);
5889
5890 log << "After loan creation:" << std::endl;
5891 log << " AssetsTotal: " << assetsTotal << std::endl;
5892 log << " AssetsAvailable: " << assetsAvail << std::endl;
5893 log << " Difference: " << (assetsTotal - assetsAvail) << std::endl;
5894
5895 auto const loanSle = env.le(loanKeylet);
5896 if (!BEAST_EXPECT(loanSle))
5897 return;
5898 auto const state = constructRoundedLoanState(loanSle);
5899
5900 log << "Loan state:" << std::endl;
5901 log << " ValueOutstanding: " << state.valueOutstanding << std::endl;
5902 log << " PrincipalOutstanding: " << state.principalOutstanding << std::endl;
5903 log << " InterestOutstanding: " << state.interestOutstanding() << std::endl;
5904 log << " InterestDue: " << state.interestDue << std::endl;
5905 log << " FeeDue: " << state.managementFeeDue << std::endl;
5906
5907 // after loan creation the assets total and available should
5908 // reflect the value of the loan
5909 BEAST_EXPECT(assetsAvail < assetsTotal);
5910 BEAST_EXPECT(
5911 assetsAvail ==
5912 broker.asset(brokerParams.vaultDeposit - loanParams.principalRequest).number());
5913 BEAST_EXPECT(
5914 assetsTotal ==
5915 broker.asset(brokerParams.vaultDeposit + state.interestDue).number());
5916 }
5917
5918 // Step 7: Trigger default (dust adjustment will occur)
5919 env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault));
5920 env.close();
5921
5922 // Step 8: Verify phantom assets created
5923 {
5924 auto const vaultSle2 = env.le(vaultKeylet);
5925 Number const assetsTotal2 = vaultSle2->at(sfAssetsTotal);
5926 Number const assetsAvail2 = vaultSle2->at(sfAssetsAvailable);
5927
5928 log << "After default:" << std::endl;
5929 log << " AssetsTotal: " << assetsTotal2 << std::endl;
5930 log << " AssetsAvailable: " << assetsAvail2 << std::endl;
5931 log << " Difference: " << (assetsTotal2 - assetsAvail2) << std::endl;
5932
5933 // after a default the assets total and available should be equal
5934 BEAST_EXPECT(assetsAvail2 == assetsTotal2);
5935 }
5936 }
5937
5938 void
5940 {
5941 using namespace jtx;
5942
5943 testcase("RIPD-3831");
5944
5945 Account const issuer("issuer");
5946 Account const lender("lender");
5947 Account const borrower("borrower");
5948
5949 BrokerParameters const brokerParams{
5950 .vaultDeposit = 100000,
5951 .debtMax = 0,
5952 .coverRateMin = TenthBips32{0},
5953 // .managementFeeRate = TenthBips16{5919},
5954 .coverRateLiquidation = TenthBips32{0}};
5955 LoanParameters const loanParams{
5956 .account = lender,
5957 .counter = borrower,
5958 .principalRequest = Number{200'000, -6},
5959 .lateFee = Number{200, -6},
5960 .interest = TenthBips32{50'000},
5961 .payTotal = 10,
5962 .payInterval = 150};
5963
5964 auto const assetType = AssetType::XRP;
5965
5966 Env env(*this, all);
5967
5968 auto loanResult =
5969 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
5970
5971 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
5972 return;
5973
5974 auto broker = std::get<BrokerInfo>(*loanResult);
5975 auto loanKeylet = std::get<Keylet>(*loanResult);
5976
5977 using tp = NetClock::time_point;
5978 using d = NetClock::duration;
5979
5980 auto state = getCurrentState(env, broker, loanKeylet);
5981 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
5982 {
5983 env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
5984 }
5985
5986 topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee);
5987
5988 using namespace jtx::loan;
5989
5990 auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue)));
5991
5992 {
5993 auto const submitParam = to_string(jv);
5994 auto const jr = env.rpc("submit", borrower.name(), submitParam);
5995
5996 BEAST_EXPECT(jr.isMember(jss::result));
5997 auto const jResult = jr[jss::result];
5998 }
5999
6000 env.close();
6001
6002 // Make sure the system keeps responding
6003 env(noop(borrower));
6004 env.close();
6005 env(noop(issuer));
6006 env.close();
6007 env(noop(lender));
6008 env.close();
6009 }
6010
6011 void
6013 {
6014 testcase("RIPD-3459 - LoanBroker incorrect debt total");
6015
6016 using namespace jtx;
6017
6018 Account const issuer("issuer");
6019 Account const lender("lender");
6020 Account const borrower("borrower");
6021
6022 BrokerParameters const brokerParams{
6023 .vaultDeposit = 200'000,
6024 .debtMax = 0,
6025 .coverRateMin = TenthBips32{0},
6026 .managementFeeRate = TenthBips16{500},
6027 .coverRateLiquidation = TenthBips32{0}};
6028 LoanParameters const loanParams{
6029 .account = lender,
6030 .counter = borrower,
6031 .principalRequest = Number{100'000, -4},
6032 .interest = TenthBips32{100'000},
6033 .payTotal = 10};
6034
6035 auto const assetType = AssetType::MPT;
6036
6037 Env env(*this, all);
6038
6039 auto loanResult =
6040 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6041
6042 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6043 return;
6044
6045 auto broker = std::get<BrokerInfo>(*loanResult);
6046 auto loanKeylet = std::get<Keylet>(*loanResult);
6047 auto pseudoAcct = std::get<Account>(*loanResult);
6048
6049 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6050
6051 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
6052 {
6053 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
6054 {
6055 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
6056 }
6057 }
6058
6059 makeLoanPayments(
6060 env,
6061 broker,
6062 loanParams,
6063 loanKeylet,
6064 verifyLoanStatus,
6065 issuer,
6066 lender,
6067 borrower,
6069
6070 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
6071 {
6072 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
6073 {
6074 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
6075 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::zero);
6076 }
6077 }
6078 }
6079
6080 void
6082 {
6083 testcase("Crash with tfLoanOverpayment");
6084 using namespace jtx;
6085 using namespace loan;
6086 Account const lender{"lender"};
6087 Account const issuer{"issuer"};
6088 Account const borrower{"borrower"};
6089 Account const depositor{"depositor"};
6090 auto const txfee = fee(XRP(100));
6091
6092 Env env(*this);
6093 Vault const vault(env);
6094
6095 env.fund(XRP(10'000), lender, issuer, borrower, depositor);
6096 env.close();
6097
6098 auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()});
6099 env(tx, txfee);
6100 env.close();
6101
6102 env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}),
6103 txfee);
6104 env.close();
6105
6106 auto const brokerKeyLet = keylet::loanbroker(lender.id(), env.seq(lender));
6107
6108 env(loanBroker::set(lender, vaultKeyLet.key), txfee);
6109 env.close();
6110
6111 // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}};
6112
6113 STAmount const debtMaximumRequest = XRPAmount(200'000);
6114
6115 env(set(borrower, brokerKeyLet.key, debtMaximumRequest),
6116 sig(sfCounterpartySignature, lender),
6117 interestRate(TenthBips32(50'000)),
6118 paymentTotal(2),
6119 paymentInterval(150),
6120 txflags(tfLoanOverpayment),
6121 txfee);
6122 env.close();
6123
6124 std::uint32_t const loanSequence = 1;
6125 auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence);
6126
6127 if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
6128 {
6129 env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)),
6130 txflags(tfLoanOverpayment),
6131 txfee);
6132 env.close();
6133 }
6134 }
6135
6136 void
6138 {
6139 testcase("Minimum cover rounding allows undercoverage (XRP)");
6140
6141 using namespace jtx;
6142 using namespace loanBroker;
6143
6144 Env env(*this, all);
6145
6146 Account const lender{"lender"};
6147 Account const borrower{"borrower"};
6148
6149 env.fund(XRP(200'000), lender, borrower);
6150 env.close();
6151
6152 // Vault with XRP asset
6153 Vault const vault{env};
6154 auto [vaultCreate, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()});
6155 env(vaultCreate);
6156 env.close();
6157 BEAST_EXPECT(env.le(vaultKeylet));
6158
6159 // Seed the vault with XRP so it can fund the loan principal
6160 PrettyAsset const xrpAsset{xrpIssue(), 1};
6161
6162 BrokerParameters const brokerParams{
6163 .vaultDeposit = 1'000,
6164 .debtMax = Number{0},
6165 .coverRateMin = TenthBips32{10'000},
6166 .coverDeposit = 82,
6167 };
6168
6169 auto const brokerInfo = createVaultAndBroker(env, xrpAsset, lender, brokerParams);
6170 // Create a loan with principal 804 XRP and 0% interest (so
6171 // DebtTotal increases by exactly 804)
6172 env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()),
6173 loan::interestRate(TenthBips32(0)),
6174 sig(sfCounterpartySignature, lender),
6175 fee(env.current()->fees().base * 2));
6176 BEAST_EXPECT(env.ter() == tesSUCCESS);
6177 env.close();
6178
6179 // Verify DebtTotal is exactly 804
6180 if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
6181 BEAST_EXPECT(brokerSle))
6182 {
6183 log << *brokerSle << std::endl;
6184 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804));
6185 }
6186
6187 // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable.
6188 // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails.
6189 env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()),
6191 BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS);
6192 env.close();
6193
6194 // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable.
6195 // because that leaves sufficient cover, this succeeds
6196 env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value()));
6197 BEAST_EXPECT(env.ter() == tesSUCCESS);
6198 env.close();
6199
6200 // Validate CoverAvailable == 80 XRP and DebtTotal remains 804
6201 if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
6202 BEAST_EXPECT(brokerSle))
6203 {
6204 log << *brokerSle << std::endl;
6205 BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == xrpAsset(81).value());
6206 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804));
6207
6208 // Also demonstrate that the true minimum (804 * 10%) exceeds 80
6209 auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000));
6210 log << "Theoretical min cover: " << theoreticalMin << std::endl;
6211 BEAST_EXPECT(Number(804, -1) == theoreticalMin);
6212 }
6213 }
6214
6215 void
6217 {
6218 testcase("RIPD-3902 - 1 IOU loan payments");
6219
6220 using namespace jtx;
6221
6222 Account const issuer("issuer");
6223 Account const lender("lender");
6224 Account const borrower("borrower");
6225
6226 BrokerParameters const brokerParams{
6227 .vaultDeposit = 10,
6228 .debtMax = 0,
6229 .coverRateMin = TenthBips32{0},
6230 .managementFeeRate = TenthBips16{0},
6231 .coverRateLiquidation = TenthBips32{0}};
6232 LoanParameters const loanParams{
6233 .account = lender,
6234 .counter = borrower,
6235 .principalRequest = Number{1, 0},
6236 .interest = TenthBips32{100'000},
6237 .payTotal = 5,
6238 .payInterval = 150,
6239 .gracePd = 60};
6240
6241 auto const assetType = AssetType::IOU;
6242
6243 Env env(*this, all);
6244
6245 auto loanResult =
6246 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6247
6248 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6249 return;
6250
6251 auto broker = std::get<BrokerInfo>(*loanResult);
6252 auto loanKeylet = std::get<Keylet>(*loanResult);
6253 auto pseudoAcct = std::get<Account>(*loanResult);
6254
6255 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6256
6257 makeLoanPayments(
6258 env,
6259 broker,
6260 loanParams,
6261 loanKeylet,
6262 verifyLoanStatus,
6263 issuer,
6264 lender,
6265 borrower,
6267 }
6268
6269 void
6271 {
6272 testcase("Test Borrower is Broker");
6273 using namespace jtx;
6274 using namespace loan;
6275 Account const broker{"broker"};
6276 Account const issuer{"issuer"};
6277 Account const borrower_{"borrower"};
6278 Account const depositor{"depositor"};
6279
6280 auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) {
6281 Env env(*this);
6282 Vault const vault(env);
6283
6284 if (borrower == broker)
6285 {
6286 env.fund(XRP(10'000), broker, issuer, depositor);
6287 }
6288 else
6289 {
6290 env.fund(XRP(10'000), broker, borrower, issuer, depositor);
6291 }
6292 env.close();
6293
6294 auto const xrpFee = XRP(100);
6295 auto const txFee = fee(xrpFee);
6296
6297 STAmount const debtMaximumRequest = getMaxDebt(env);
6298
6299 auto const& asset = debtMaximumRequest.asset();
6300 auto const initialVault = asset(debtMaximumRequest * 100);
6301
6302 auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
6303 env(tx, txFee);
6304 env.close();
6305
6306 env(vault.deposit(
6307 {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}),
6308 txFee);
6309 env.close();
6310
6311 auto const brokerKeylet = keylet::loanbroker(broker.id(), env.seq(broker));
6312
6313 env(loanBroker::set(broker, vaultKeylet.key), txFee);
6314 env.close();
6315
6316 auto const serviceFee = 101;
6317
6318 env(set(broker, brokerKeylet.key, debtMaximumRequest),
6319 counterparty(borrower),
6320 sig(sfCounterpartySignature, borrower),
6321 loanServiceFee(serviceFee),
6322 paymentTotal(10),
6323 txFee);
6324 env.close();
6325
6326 std::uint32_t const loanSequence = 1;
6327 auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence);
6328
6329 auto const brokerBalanceBefore = env.balance(broker, asset);
6330
6331 if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle))
6332 {
6333 auto const payment = loanSle->at(sfPeriodicPayment);
6334 auto const totalPayment = payment + serviceFee;
6335 env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee);
6336 env.close();
6337 if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
6338 {
6339 auto const expected = [&]() {
6340 // The service fee is transferred to the broker if
6341 // a borrower is not the broker
6342 if (borrower != broker)
6343 return brokerBalanceBefore.number() + serviceFee;
6344 // Since a borrower is the broker, the payment is
6345 // transferred to the Vault from the broker but not
6346 // the service fee.
6347 // If the asset is XRP then the broker pays the txfee.
6348 if (asset.native())
6349 return brokerBalanceBefore.number() - payment - xrpFee.number();
6350 return brokerBalanceBefore.number() - payment;
6351 }();
6352 BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value());
6353 }
6354 }
6355 };
6356 // Test when a borrower is the broker and is not to verify correct
6357 // service fee transfer in both cases.
6358 for (auto const& borrowerAcct : {broker, borrower_})
6359 {
6360 testLoanAsset(
6361 [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct);
6362 testLoanAsset(
6363 [&](Env& env) -> STAmount {
6364 auto const IOU = issuer["USD"];
6365 env(trust(broker, IOU(1'000'000'000)));
6366 env(trust(depositor, IOU(1'000'000'000)));
6367 env(pay(issuer, broker, IOU(100'000'000)));
6368 env(pay(issuer, depositor, IOU(100'000'000)));
6369 env.close();
6370 return IOU(200'000);
6371 },
6372 borrowerAcct);
6373 testLoanAsset(
6374 [&](Env& env) -> STAmount {
6375 MPTTester const mpt(
6376 {.env = env,
6377 .issuer = issuer,
6378 .holders = {broker, depositor},
6379 .pay = 100'000'000});
6380 return mpt(200'000);
6381 },
6382 borrowerAcct);
6383 }
6384 }
6385
6386 void
6388 {
6389 testcase("RIPD-4096 - Issuer as borrower");
6390
6391 using namespace jtx;
6392
6393 Account const issuer("issuer");
6394 Account const lender("lender");
6395
6396 BrokerParameters const brokerParams{
6397 .vaultDeposit = 100'000,
6398 .debtMax = 0,
6399 .coverRateMin = TenthBips32{0},
6400 .managementFeeRate = TenthBips16{0},
6401 .coverRateLiquidation = TenthBips32{0}};
6402 LoanParameters const loanParams{
6403 .account = lender, .counter = issuer, .principalRequest = Number{10000}};
6404
6405 auto const assetType = AssetType::IOU;
6406
6407 Env env(*this, all);
6408
6409 auto loanResult =
6410 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer);
6411
6412 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6413 return;
6414
6415 auto broker = std::get<BrokerInfo>(*loanResult);
6416 auto loanKeylet = std::get<Keylet>(*loanResult);
6417 auto pseudoAcct = std::get<Account>(*loanResult);
6418
6419 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6420
6421 makeLoanPayments(
6422 env,
6423 broker,
6424 loanParams,
6425 loanKeylet,
6426 verifyLoanStatus,
6427 issuer,
6428 lender,
6429 issuer,
6431 }
6432
6433 void
6435 {
6436 testcase("RIPD-4125 - overpayment");
6437
6438 using namespace jtx;
6439
6440 Account const issuer("issuer");
6441 Account const lender("lender");
6442 Account const borrower("borrower");
6443
6444 BrokerParameters const brokerParams{
6445 .vaultDeposit = 100'000,
6446 .debtMax = 0,
6447 .coverRateMin = TenthBips32{0},
6448 .managementFeeRate = TenthBips16{0},
6449 .coverRateLiquidation = TenthBips32{0}};
6450 LoanParameters const loanParams{
6451 .account = lender,
6452 .counter = borrower,
6453 .principalRequest = Number{200000, -6},
6454 .interest = TenthBips32{50000},
6455 .payTotal = 3,
6456 .payInterval = 200,
6457 .gracePd = 60,
6458 .flags = tfLoanOverpayment,
6459 };
6460
6461 auto const assetType = AssetType::XRP;
6462
6463 Env env(*this, makeConfig(), all, nullptr, beast::severities::Severity::kWarning);
6464
6465 auto loanResult =
6466 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6467
6468 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6469 return;
6470
6471 auto broker = std::get<BrokerInfo>(*loanResult);
6472 auto loanKeylet = std::get<Keylet>(*loanResult);
6473 auto pseudoAcct = std::get<Account>(*loanResult);
6474
6475 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6476
6477 auto const state = getCurrentState(env, broker, loanKeylet);
6478
6479 env(loan::pay(
6480 borrower,
6481 loanKeylet.key,
6482 STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1},
6483 tfLoanOverpayment));
6484 env.close();
6485
6486 PaymentParameters const paymentParams{
6487 .showStepBalances = false,
6488 .validateBalances = true,
6489 };
6490
6491 makeLoanPayments(
6492 env,
6493 broker,
6494 loanParams,
6495 loanKeylet,
6496 verifyLoanStatus,
6497 issuer,
6498 lender,
6499 borrower,
6500 paymentParams);
6501 }
6502
6503 void
6505 {
6506 testcase("testOverpaymentManagementFee");
6507
6508 using namespace jtx;
6509 using namespace loan;
6510
6511 Env env(*this, all);
6512
6513 Account const lender{"lender"}, borrower{"borrower"};
6514
6515 env.fund(XRP(10'000'000), lender, borrower);
6516 env.close();
6517
6518 PrettyAsset const asset{xrpIssue(), 1000};
6519
6520 auto const result = createVaultAndBroker(
6521 env,
6522 asset,
6523 lender,
6524 {
6525 .vaultDeposit = asset(100'000).value(),
6526 .managementFeeRate = TenthBips16(10'000),
6527 });
6528
6529 auto const loanSetFee = fee(env.current()->fees().base * 2);
6530
6531 auto const loanKeylet = keylet::loan(
6532 result.brokerKeylet().key, (env.le(result.brokerKeylet()))->at(sfLoanSequence));
6533 env(loan::set(
6534 borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment),
6535 sig(sfCounterpartySignature, lender),
6536 loan::paymentInterval(86400 * 30),
6537 loan::paymentTotal(3),
6538 loan::overpaymentInterestRate(TenthBips32(percentageToTenthBips(20))),
6539 loanSetFee);
6540
6541 // From calculator
6542 auto const expectedOverpaymentManagementFee = Number{33333, 0};
6543 auto const loanBrokerBalanceBefore = env.balance(lender);
6544
6545 auto const loanPayFee = fee(env.current()->fees().base * 2);
6546 env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee);
6547 env.close();
6548
6549 BEAST_EXPECTS(
6550 env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee,
6551 "overpayment management fee missmatch; expected:" +
6552 to_string(expectedOverpaymentManagementFee) +
6553 " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore));
6554 }
6555
6556 void
6558 {
6559 testcase << "LoanPay Broker Owner Missing Trustline (PoC)";
6560 using namespace jtx;
6561 using namespace loan;
6562 Account const issuer("issuer");
6563 Account const borrower("borrower");
6564 Account const broker("broker");
6565 auto const IOU = issuer["IOU"];
6566 Env env(*this, all);
6567 env.fund(XRP(20'000), issuer, broker, borrower);
6568 env.close();
6569 // Set up trustlines and fund accounts
6570 env(trust(broker, IOU(20'000'000)));
6571 env(trust(borrower, IOU(20'000'000)));
6572 env(pay(issuer, broker, IOU(10'000'000)));
6573 env(pay(issuer, borrower, IOU(1'000)));
6574 env.close();
6575 // Create vault and broker
6576 auto const brokerInfo = createVaultAndBroker(env, IOU, broker);
6577 // Create a loan first (this creates debt)
6578 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
6579 env(set(borrower, brokerInfo.brokerID, 10'000),
6580 sig(sfCounterpartySignature, broker),
6581 loanServiceFee(IOU(100).value()),
6582 paymentInterval(100),
6583 fee(XRP(100)));
6584 env.close();
6585 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
6586 // We need coverAvailable >= (debtTotal * coverRateMinimum)
6587 // Deposit enough cover to ensure the fee goes to broker owner
6588 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
6589 // at least 1,000 cover. Default cover is 1,000, so we add more to be
6590 // safe.
6591 auto const additionalCover = IOU(50'000).value();
6592 env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{IOU, additionalCover}));
6593 env.close();
6594 // Verify broker owner has a trustline
6595 auto const brokerTrustline = keylet::line(broker, IOU);
6596 BEAST_EXPECT(env.le(brokerTrustline) != nullptr);
6597 // Broker owner deletes their trustline
6598 // First, pay any positive balance to issuer to zero it out
6599 auto const brokerBalance = env.balance(broker, IOU);
6600 env(pay(broker, issuer, brokerBalance));
6601 env.close();
6602 // Remove the trustline by setting limit to 0
6603 env(trust(broker, IOU(0)));
6604 env.close();
6605 // Verify trustline is deleted
6606 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
6607 // Now borrower tries to make a payment
6608 // We should get a tesSUCCESS instead of a tecNO_LINE.
6609 env(pay(borrower, keylet.key, IOU(10'100)), fee(XRP(100)), ter(tesSUCCESS));
6610 env.close();
6611 // Verify trustline is still deleted
6612 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
6613 // Verify the service fee went to the broker pseudo-account
6614 if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
6615 BEAST_EXPECT(brokerSle))
6616 {
6617 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
6618 auto const balance = env.balance(pseudo, IOU);
6619 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
6620 BEAST_EXPECTS(balance == IOU(51'100), to_string(Json::Value(balance)));
6621 }
6622 }
6623
6624 void
6626 {
6627 testcase << "LoanPay Broker Owner MPT unauthorized";
6628 using namespace jtx;
6629 using namespace loan;
6630
6631 Account const issuer("issuer");
6632 Account const borrower("borrower");
6633 Account const broker("broker");
6634
6635 Env env(*this, all);
6636 env.fund(XRP(20'000), issuer, broker, borrower);
6637 env.close();
6638
6639 MPTTester mptt{env, issuer, mptInitNoFund};
6640 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
6641
6642 PrettyAsset const MPT{mptt.issuanceID()};
6643
6644 // Authorize broker and borrower
6645 mptt.authorize({.account = broker});
6646 mptt.authorize({.account = borrower});
6647
6648 env.close();
6649
6650 // Fund accounts
6651 env(pay(issuer, broker, MPT(10'000'000)));
6652 env(pay(issuer, borrower, MPT(1'000)));
6653 env.close();
6654
6655 // Create vault and broker
6656 auto const brokerInfo = createVaultAndBroker(env, MPT, broker);
6657 // Create a loan first (this creates debt)
6658 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
6659 env(set(borrower, brokerInfo.brokerID, 10'000),
6660 sig(sfCounterpartySignature, broker),
6661 loanServiceFee(MPT(100).value()),
6662 paymentInterval(100),
6663 fee(XRP(100)));
6664 env.close();
6665 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
6666 // We need coverAvailable >= (debtTotal * coverRateMinimum)
6667 // Deposit enough cover to ensure the fee goes to broker owner
6668 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
6669 // at least 1,000 cover. Default cover is 1,000, so we add more to be
6670 // safe.
6671 auto const additionalCover = MPT(50'000).value();
6672 env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{MPT, additionalCover}));
6673 env.close();
6674 // Verify broker owner is authorized
6675 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
6676 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
6677 // Broker owner unauthorizes.
6678 // First, pay any positive balance to issuer to zero it out
6679 auto const brokerBalance = env.balance(broker, MPT);
6680 env(pay(broker, issuer, brokerBalance));
6681 env.close();
6682 // Then, unauthorize the MPT.
6683 mptt.authorize({.account = broker, .flags = tfMPTUnauthorize});
6684 env.close();
6685 // Verify the MPT is unauthorized.
6686 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
6687 // Now borrower tries to make a payment
6688 // We should get a tesSUCCESS instead of a tecNO_AUTH.
6689 auto const borrowerBalance = env.balance(borrower, MPT);
6690 env(pay(borrower, keylet.key, MPT(10'100)), fee(XRP(100)), ter(tesSUCCESS));
6691 env.close();
6692 // Verify the MPT is still unauthorized.
6693 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
6694 // Verify the service fee went to the broker pseudo-account
6695 if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
6696 BEAST_EXPECT(brokerSle))
6697 {
6698 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
6699 auto const balance = env.balance(pseudo, MPT);
6700 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
6701 BEAST_EXPECTS(balance == MPT(51'100), to_string(Json::Value(balance)));
6702 }
6703 }
6704
6705 void
6707 {
6708 testcase << "LoanPay Broker Owner without permissioned domain of the MPT";
6709 using namespace jtx;
6710 using namespace loan;
6711
6712 Account const issuer("issuer");
6713 Account const borrower("borrower");
6714 Account const broker("broker");
6715
6716 Env env(*this, all);
6717 env.fund(XRP(20'000), issuer, broker, borrower);
6718 env.close();
6719
6720 auto credType = "credential1";
6721
6722 pdomain::Credentials const credentials1{{issuer, credType}};
6723 env(pdomain::setTx(issuer, credentials1));
6724 env.close();
6725
6726 auto domainID = pdomain::getNewDomain(env.meta());
6727
6728 env(credentials::create(broker, issuer, credType));
6729 env(credentials::accept(broker, issuer, credType));
6730 env.close();
6731
6732 env(credentials::create(borrower, issuer, credType));
6733 env(credentials::accept(borrower, issuer, credType));
6734 env.close();
6735
6736 MPTTester mptt{env, issuer, mptInitNoFund};
6737 mptt.create({
6738 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
6739 .domainID = domainID,
6740 });
6741
6742 PrettyAsset const MPT{mptt.issuanceID()};
6743
6744 // Authorize broker and borrower
6745 mptt.authorize({.account = broker});
6746 mptt.authorize({.account = borrower});
6747
6748 env.close();
6749
6750 // Fund accounts
6751 env(pay(issuer, broker, MPT(10'000'000)));
6752 env(pay(issuer, borrower, MPT(1'000)));
6753 env.close();
6754
6755 // Create vault and broker
6756 auto const brokerInfo = createVaultAndBroker(env, MPT, broker);
6757 // Create a loan first (this creates debt)
6758 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
6759 env(set(borrower, brokerInfo.brokerID, 10'000),
6760 sig(sfCounterpartySignature, broker),
6761 loanServiceFee(MPT(100).value()),
6762 paymentInterval(100),
6763 fee(XRP(100)));
6764 env.close();
6765 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
6766 // We need coverAvailable >= (debtTotal * coverRateMinimum)
6767 // Deposit enough cover to ensure the fee goes to broker owner
6768 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
6769 // at least 1,000 cover. Default cover is 1,000, so we add more to be
6770 // safe.
6771 auto const additionalCover = MPT(50'000).value();
6772 env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{MPT, additionalCover}));
6773 env.close();
6774 // Verify broker owner is authorized
6775 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
6776 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
6777 // Remove the credentials for the Broker owner.
6778 // First, pay any positive balance to issuer to zero it out
6779 auto const brokerBalance = env.balance(broker, MPT);
6780 env(pay(broker, issuer, brokerBalance));
6781 env.close();
6782
6783 env(credentials::deleteCred(broker, broker, issuer, credType));
6784 env.close();
6785
6786 // Make sure the broker is not authorized to hold the MPT after we
6787 // deleted the credentials
6788 env(pay(issuer, broker, MPT(1'000)), ter(tecNO_AUTH));
6789
6790 // Now borrower tries to make a payment
6791 // We should get a tesSUCCESS instead of a tecNO_AUTH.
6792 auto const borrowerBalance = env.balance(borrower, MPT);
6793 env(pay(borrower, keylet.key, MPT(10'100)), fee(XRP(100)), ter(tesSUCCESS));
6794 env.close();
6795 // Verify broker is still not authorized
6796 env(pay(issuer, broker, MPT(1'000)), ter(tecNO_AUTH));
6797 // Verify the service fee went to the broker pseudo-account
6798 if (auto const brokerSle = env.le(keylet::loanbroker(brokerInfo.brokerID));
6799 BEAST_EXPECT(brokerSle))
6800 {
6801 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
6802 auto const balance = env.balance(pseudo, MPT);
6803 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
6804 BEAST_EXPECTS(balance == MPT(51'100), to_string(Json::Value(balance)));
6805 }
6806 }
6807
6808 void
6810 {
6811 testcase << "LoanSet Broker Owner without permissioned domain of the MPT";
6812 using namespace jtx;
6813 using namespace loan;
6814
6815 Account const issuer("issuer");
6816 Account const borrower("borrower");
6817 Account const broker("broker");
6818
6819 Env env(*this, all);
6820 env.fund(XRP(20'000), issuer, broker, borrower);
6821 env.close();
6822
6823 auto credType = "credential1";
6824
6825 pdomain::Credentials const credentials1{{issuer, credType}};
6826 env(pdomain::setTx(issuer, credentials1));
6827 env.close();
6828
6829 auto domainID = pdomain::getNewDomain(env.meta());
6830
6831 // Add credentials for the broker and borrower
6832 env(credentials::create(broker, issuer, credType));
6833 env(credentials::accept(broker, issuer, credType));
6834 env.close();
6835
6836 env(credentials::create(borrower, issuer, credType));
6837 env(credentials::accept(borrower, issuer, credType));
6838 env.close();
6839
6840 MPTTester mptt{env, issuer, mptInitNoFund};
6841 mptt.create({
6842 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
6843 .domainID = domainID,
6844 });
6845
6846 PrettyAsset const MPT{mptt.issuanceID()};
6847
6848 // Authorize broker and borrower
6849 mptt.authorize({.account = broker});
6850 mptt.authorize({.account = borrower});
6851 env.close();
6852
6853 // Fund accounts
6854 env(pay(issuer, broker, MPT(10'000'000)));
6855 env(pay(issuer, borrower, MPT(1'000)));
6856 env.close();
6857
6858 // Create vault and broker
6859 auto const brokerInfo = createVaultAndBroker(env, MPT, broker);
6860
6861 // Remove the credentials for the Broker owner.
6862 // Clear the balance first.
6863 auto const brokerBalance = env.balance(broker, MPT);
6864 env(pay(broker, issuer, brokerBalance));
6865 env.close();
6866 // Delete the credentials
6867 env(credentials::deleteCred(broker, broker, issuer, credType));
6868 env.close();
6869
6870 // Create a loan, this should fail for tecNO_AUTH
6871 env(set(borrower, brokerInfo.brokerID, 10'000),
6872 sig(sfCounterpartySignature, broker),
6873 loanServiceFee(MPT(100).value()),
6874 paymentInterval(100),
6875 fee(XRP(100)),
6876 ter(tecNO_AUTH));
6877 env.close();
6878 }
6879
6880 void
6882 {
6883 testcase << "First-Loss Capital Depletion on Sequential Defaults";
6884
6885 using namespace jtx;
6886 using namespace loan;
6887 using namespace loanBroker;
6888
6889 Env env(*this, all);
6890
6891 Account const issuer{"issuer"};
6892 Account const lender{"lender"};
6893 Account const borrowerA{"borrowerA"};
6894 Account const borrowerB{"borrowerB"};
6895
6896 env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
6897 env.close();
6898
6899 PrettyAsset const asset = xrpIssue();
6900 auto const vaultDepositAmount =
6901 asset(200'000); // Enough for 2 x 50k loans plus interest/fees
6902
6903 auto const brokerInfo = createVaultAndBroker(
6904 env,
6905 asset,
6906 lender,
6907 {
6908 .vaultDeposit = vaultDepositAmount.value(),
6909 .debtMax = 0,
6910 .coverRateMin = TenthBips32(20000), // 20%
6911 .coverDeposit = 21'000,
6912 .managementFeeRate = TenthBips16(100), // 0.1%
6913 .coverRateLiquidation = TenthBips32(100000),
6914 });
6915 auto const brokerKeylet = brokerInfo.brokerKeylet();
6916
6917 // Create two identical loans: each 50,000 XRP principal (scaled down to
6918 // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal
6919 // + interest) Formula will calculate cover as: 100% × (20% × 100,000) =
6920 // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first
6921 // default
6922 auto const principalAmount = Number(50'000);
6923 auto const loanPaymentInterval = 2592000; // 30 days
6924 auto const loanGracePeriod = 604800; // 7 days
6925
6926 // Create Loan A
6927 auto loanATx = env.jt(
6928 set(borrowerA, brokerKeylet.key, principalAmount),
6929 sig(sfCounterpartySignature, lender),
6930 interestRate(TenthBips32(500)), // 5%
6931 paymentTotal(12),
6932 loan::paymentInterval(loanPaymentInterval),
6933 loan::gracePeriod(loanGracePeriod),
6934 fee(XRP(10))); // Sufficient fee for multi-sig transaction
6935 env(loanATx);
6936 env.close();
6937
6938 auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1);
6939
6940 // Create Loan B
6941 auto loanBTx = env.jt(
6942 set(borrowerB, brokerKeylet.key, principalAmount),
6943 sig(sfCounterpartySignature, lender),
6944 interestRate(TenthBips32(500)), // 5%
6945 paymentTotal(12),
6946 loan::paymentInterval(loanPaymentInterval),
6947 loan::gracePeriod(loanGracePeriod),
6948 fee(XRP(10))); // Sufficient fee for multi-sig transaction
6949 env(loanBTx);
6950 env.close();
6951
6952 auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2);
6953
6954 auto loanASle = env.le(loanAKeylet);
6955 if (!BEAST_EXPECT(loanASle))
6956 return;
6957
6958 // Advance time past grace period for both loans to be defaultable
6959 auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
6960 auto const loanAGrace = loanASle->at(sfGracePeriod);
6961 env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60});
6962
6963 env(manage(lender, loanAKeylet.key, tfLoanDefault), ter(tesSUCCESS));
6964 env.close();
6965
6966 // Verify Loan A is defaulted
6967 loanASle = env.le(loanAKeylet);
6968 if (!BEAST_EXPECT(loanASle))
6969 return;
6970 BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
6971 BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
6972
6973 // Check broker state after first default (from committed ledger)
6974 auto brokerSle = env.le(brokerKeylet);
6975 if (!BEAST_EXPECT(brokerSle))
6976 return;
6977 auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
6978 auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
6979
6980 // DebtTotal should have decreased by Loan A's debt
6981 BEAST_EXPECT(afterFirstDebtTotal == 50'134);
6982
6983 // CoverAvailable should have decreased significantly
6984 BEAST_EXPECT(afterFirstCoverAvailable == 946);
6985
6986 env(manage(lender, loanBKeylet.key, tfLoanDefault), ter(tesSUCCESS));
6987
6988 brokerSle = env.le(brokerKeylet);
6989 if (!BEAST_EXPECT(brokerSle))
6990 return;
6991 auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
6992 auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
6993
6994 BEAST_EXPECT(afterSecondDebtTotal == 0);
6995
6996 BEAST_EXPECT(afterSecondCoverAvailable == 0);
6997 }
6998
6999public:
7000 void
7001 run() override
7002 {
7003#if LOANTODO
7004 testLoanPayLateFullPaymentBypassesPenalties();
7005 testLoanCoverMinimumRoundingExploit();
7006#endif
7007 testInvalidLoanSet();
7008
7009 testCoverDepositWithdrawNonTransferableMPT();
7010 testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic();
7011
7012 testDisabled();
7013 testSelfLoan();
7014 testIssuerLoan();
7015 testLoanSet();
7016 testLifecycle();
7017 testServiceFeeOnBrokerDeepFreeze();
7018
7019 testRPC();
7020 testInvalidLoanDelete();
7021 testInvalidLoanManage();
7022 testInvalidLoanPay();
7023
7024 testBatchBypassCounterparty();
7025 testLoanPayComputePeriodicPaymentValidRateInvariant();
7026 testAccountSendMptMinAmountInvariant();
7027 testLoanPayDebtDecreaseInvariant();
7028 testWrongMaxDebtBehavior();
7029 testLoanPayComputePeriodicPaymentValidTotalInterestInvariant();
7030 testDosLoanPay();
7031 testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant();
7032 testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant();
7033 testLoanNextPaymentDueDateOverflow();
7034
7035 testRequireAuth();
7036 testDustManipulation();
7037
7038 testRIPD3831();
7039 testRIPD3459();
7040 testRIPD3901();
7041 testRIPD3902();
7042 testRoundingAllowsUndercoverage();
7043 testBorrowerIsBroker();
7044 testIssuerIsBorrower();
7045 testLimitExceeded();
7046 testOverpaymentManagementFee();
7047 testLoanPayBrokerOwnerMissingTrustline();
7048 testLoanPayBrokerOwnerUnauthorizedMPT();
7049 testLoanPayBrokerOwnerNoPermissionedDomainMPT();
7050 testLoanSetBrokerOwnerNoPermissionedDomainMPT();
7051 testSequentialFLCDepletion();
7052 }
7053};
7054
7056{
7057protected:
7059
7061 std::uniform_int_distribution<std::int64_t> principalDist{100'000, 1'000'000'000};
7063 std::uniform_int_distribution<> paymentTotalDist{12, 10000};
7064 std::uniform_int_distribution<> paymentIntervalDist{60, 3600 * 24 * 30};
7065 std::uniform_int_distribution<std::uint16_t> managementFeeRateDist{0, 10'000};
7066 std::uniform_int_distribution<> serviceFeeDist{0, 20};
7067 /*
7068 # Generate parameters that are more likely to be valid
7069 principal = Decimal(str(rand.randint(100000,
7070 100'000'000))).quantize(ROUND_TARGET)
7071
7072 interest_rate = Decimal(rand.randint(1, 10000)) /
7073 Decimal(100000)
7074
7075 payment_total = rand.randint(12, 10000)
7076
7077 payment_interval = Decimal(str(rand.randint(60, 2629746)))
7078
7079 interest_fee = Decimal(rand.randint(0, 100000)) /
7080 Decimal(100000)
7081*/
7082
7083 void
7084 testRandomLoan()
7085 {
7086 using namespace jtx;
7087
7088 Account const issuer("issuer");
7089 Account const lender("lender");
7090 Account const borrower("borrower");
7091
7092 // Determine all the random parameters at once
7093 AssetType const assetType = static_cast<AssetType>(assetDist(engine_));
7094 auto const principalRequest = principalDist(engine_);
7095 TenthBips16 const managementFeeRate{managementFeeRateDist(engine_)};
7096 auto const serviceFee = serviceFeeDist(engine_);
7097 TenthBips32 interest{interestRateDist(engine_)};
7098 auto const payTotal = paymentTotalDist(engine_);
7099 auto const payInterval = paymentIntervalDist(engine_);
7100
7101 BrokerParameters const brokerParams{
7102 .vaultDeposit = principalRequest * 10,
7103 .debtMax = 0,
7104 .coverRateMin = TenthBips32{0},
7105 .managementFeeRate = managementFeeRate};
7106 LoanParameters const loanParams{
7107 .account = lender,
7108 .counter = borrower,
7109 .principalRequest = principalRequest,
7110 .serviceFee = serviceFee,
7111 .interest = interest,
7112 .payTotal = payTotal,
7113 .payInterval = payInterval,
7114 };
7115
7116 runLoan(assetType, brokerParams, loanParams);
7117 }
7118
7119public:
7120 void
7121 run() override
7122 {
7123 auto const numIterations = [s = arg()]() -> int {
7124 int const defaultNum = 5;
7125 if (s.empty())
7126 return defaultNum;
7127 try
7128 {
7129 std::size_t pos = 0;
7130 auto const r = stoi(s, &pos);
7131 if (pos != s.size())
7132 return defaultNum;
7133 return r;
7134 }
7135 catch (...)
7136 {
7137 return defaultNum;
7138 }
7139 }();
7140
7141 using namespace jtx;
7142
7143 auto const updateInterval = std::min(numIterations / 5, 100);
7144
7145 for (int i = 0; i < numIterations; ++i)
7146 {
7147 if (i % updateInterval == 0)
7148 testcase << "Random Loan Test iteration " << (i + 1) << "/" << numIterations;
7149 testRandomLoan();
7150 }
7151 }
7152};
7155{
7156 void
7157 run() override
7158 {
7159 using namespace jtx;
7160
7161 BrokerParameters const brokerParams{
7162 .vaultDeposit = 10000,
7163 .debtMax = 0,
7164 .coverRateMin = TenthBips32{0},
7165 .managementFeeRate = TenthBips16{0},
7166 .coverRateLiquidation = TenthBips32{0}};
7167 LoanParameters const loanParams{
7168 .account = Account("lender"),
7169 .counter = Account("borrower"),
7170 .principalRequest = Number{200000, -6},
7171 .interest = TenthBips32{50000},
7172 .payTotal = 2,
7173 .payInterval = 200};
7174
7175 runLoan(AssetType::XRP, brokerParams, loanParams);
7176 }
7177};
7179BEAST_DEFINE_TESTSUITE(Loan, tx, xrpl);
7180BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx, xrpl);
7181BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, xrpl);
7182
7183} // namespace test
7184} // namespace xrpl
T any_of(T... args)
Lightweight wrapper to tag static string.
Definition json_value.h:44
Represents a JSON value.
Definition json_value.h:130
Value removeMember(char const *key)
Remove and return the named member.
A testsuite class.
Definition suite.h:51
log_os< char > log
Logging output stream.
Definition suite.h:147
testcase_t testcase
Memberspace for declaring test cases.
Definition suite.h:150
bool expect(Condition const &shouldBeTrue)
Evaluate a test condition.
Definition suite.h:224
constexpr TIss const & get() const
bool native() const
Definition Asset.h:79
constexpr bool holds() const
Definition Asset.h:130
constexpr value_type const & value() const
Definition Asset.h:154
static constexpr auto disabledTxTypes
A currency issued by an account.
Definition Issue.h:13
static std::uint32_t constexpr minPaymentInterval
static std::uint32_t constexpr minPaymentTotal
static std::uint32_t constexpr defaultPaymentInterval
static std::uint32_t constexpr defaultPaymentTotal
static std::uint32_t constexpr defaultGracePeriod
std::chrono::time_point< NetClock > time_point
Definition chrono.h:46
std::chrono::duration< rep, period > duration
Definition chrono.h:45
Number is a floating point type that can represent a wide range of values.
Definition Number.h:207
Number truncate() const noexcept
Definition Number.cpp:819
constexpr int exponent() const noexcept
Returns the exponent of the external view of the Number.
Definition Number.h:573
Slice slice() const noexcept
Definition PublicKey.h:103
Asset const & asset() const
Definition STAmount.h:457
STAmount const & value() const noexcept
Definition STAmount.h:578
Blob getFieldVL(SField const &field) const
Definition STObject.cpp:641
Serializer getSerializer() const
Definition STObject.h:980
void setFieldObject(SField const &field, STObject const &v)
Definition STObject.cpp:825
STObject getFieldObject(SField const &field) const
Definition STObject.cpp:670
Slice slice() const noexcept
Definition Serializer.h:44
virtual LoadFeeTrack & getFeeTrack()=0
An immutable linear range of bytes.
Definition Slice.h:26
constexpr value_type drops() const
Returns the number of drops.
Definition XRPAmount.h:157
beast::xor_shift_engine engine_
void testCaseWrapper(jtx::Env &env, jtx::MPTTester &mptt, std::array< TAsset, NAsset > const &assets, BrokerInfo const &broker, Number const &loanAmount, int interestExponent)
Wrapper to run a series of lifecycle tests for a given asset and loan amount.
void describeLoan(jtx::Env &env, BrokerParameters const &brokerParams, LoanParameters const &loanParams, AssetType assetType, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
std::string const iouCurrency
Definition Loan_test.cpp:27
void testLoanPayBrokerOwnerNoPermissionedDomainMPT()
void testLoanPayDebtDecreaseInvariant()
void testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant()
static void topUpBorrower(jtx::Env &env, BrokerInfo const &broker, jtx::Account const &issuer, jtx::Account const &borrower, LoanState const &state, std::optional< Number > const &servFee)
FeatureBitset const all
Definition Loan_test.cpp:23
void testLoanSetBrokerOwnerNoPermissionedDomainMPT()
void run() override
Runs the suite.
void testAccountSendMptMinAmountInvariant()
bool canImpairLoan(jtx::Env const &env, BrokerInfo const &broker, LoanState const &state)
void lifecycle(std::string const &caseLabel, char const *label, jtx::Env &env, Number const &loanAmount, int interestExponent, jtx::Account const &lender, jtx::Account const &borrower, jtx::Account const &evan, BrokerInfo const &broker, jtx::Account const &pseudoAcct, std::uint32_t flags, std::function< void(Keylet const &loanKeylet, VerifyLoanStatus const &verifyLoanStatus)> toEndOfLife)
Runs through the complete lifecycle of a loan.
static std::string getCurrencyLabel(Asset const &asset)
void testRoundingAllowsUndercoverage()
void testLoanPayComputePeriodicPaymentValidTotalInterestInvariant()
void testLoanPayBrokerOwnerUnauthorizedMPT()
void testCoverDepositWithdrawNonTransferableMPT()
void testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic()
void testLoanPayComputePeriodicPaymentValidRateInvariant()
void runLoan(AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams)
LoanState getCurrentState(jtx::Env const &env, BrokerInfo const &broker, Keylet const &loanKeylet)
Get the state without checking anything.
void makeLoanPayments(jtx::Env &env, BrokerInfo const &broker, LoanParameters const &loanParams, Keylet const &loanKeylet, VerifyLoanStatus const &verifyLoanStatus, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, PaymentParameters const &paymentParams=PaymentParameters::defaults())
void testLoanNextPaymentDueDateOverflow()
void testLoanPayBrokerOwnerMissingTrustline()
void testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant()
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, VerifyLoanStatus const &verifyLoanStatus)
Get the state and check the values against the parameters used in lifecycle
jtx::PrettyAsset createAsset(jtx::Env &env, AssetType assetType, BrokerParameters const &brokerParams, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
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)
void testOverpaymentManagementFee()
void testServiceFeeOnBrokerDeepFreeze()
Immutable cryptographic account descriptor.
Definition Account.h:19
SecretKey const & sk() const
Return the secret key.
Definition Account.h:77
std::string const & human() const
Returns the human readable public key.
Definition Account.h:94
std::string const & name() const
Return the name.
Definition Account.h:63
PublicKey const & pk() const
Return the public key.
Definition Account.h:70
AccountID id() const
Returns the Account ID.
Definition Account.h:87
A transaction testing environment.
Definition Env.h:122
Application & app()
Definition Env.h:259
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:100
TER ter() const
Return the TER for the last JTx.
Definition Env.h:658
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
Definition Env.cpp:240
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
Definition Env.cpp:258
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:270
void enableFeature(uint256 const feature)
Definition Env.cpp:654
void disableFeature(uint256 const feature)
Definition Env.cpp:662
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:249
Account const & master
Definition Env.h:126
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:549
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:168
Json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:575
beast::unit_test::suite & test
Definition Env.h:124
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:301
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:847
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:483
beast::Journal const journal
Definition Env.h:163
void require(Args const &... args)
Check a set of requirements.
Definition Env.h:588
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:329
NetClock::time_point now()
Returns the current network time.
Definition Env.h:282
Converts to IOU Issue or STAmount.
void set(MPTSet const &set={})
Definition mpt.cpp:360
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:250
MPTID const & issuanceID() const
Definition mpt.h:257
Converts to MPT Issue or STAmount.
A balance matches.
Definition balance.h:19
Adds a new Batch Txn on a JTx and autofills.
Definition batch.h:34
Set the fee on a JTx.
Definition fee.h:17
Match set account flags.
Definition flags.h:108
Inject raw JSON.
Definition jtx_json.h:13
Set a multisignature on a JTx.
Definition multisign.h:41
Set the regular signature on a JTx.
Definition sig.h:15
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:15
Set the flags on a JTx.
Definition txflags.h:11
constexpr value_type value() const
Returns the underlying value.
Definition Units.h:321
T count(T... args)
T emplace_back(T... args)
T endl(T... args)
T is_same_v
T log(T... args)
T make_pair(T... args)
T make_tuple(T... args)
T max(T... args)
T min(T... args)
@ objectValue
object value (collection of name/value pairs).
Definition json_value.h:26
beast::abstract_clock< std::chrono::steady_clock > clock_type
Definition Entry.h:13
PaymentComponents computePaymentComponents(Asset const &asset, std::int32_t scale, Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentRemaining, TenthBips16 managementFeeRate)
Keylet loanbroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:510
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Definition Indexes.cpp:516
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:504
Keylet line(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:220
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:486
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:165
Json::Value trust(AccountID const &account, STAmount const &amount, std::uint32_t flags=0)
Definition AMM.cpp:802
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Definition AMM.cpp:814
Keylet keylet(test::jtx::Account const &subject, test::jtx::Account const &issuer, std::string_view credType)
Definition credentials.h:14
Json::Value coverDeposit(AccountID const &account, uint256 const &brokerID, STAmount const &amount, uint32_t flags)
Json::Value del(AccountID const &account, uint256 const &brokerID, uint32_t flags)
Json::Value coverClawback(AccountID const &account, std::uint32_t flags)
Json::Value set(AccountID const &account, uint256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
Json::Value manage(AccountID const &account, uint256 const &loanID, std::uint32_t flags)
static MPTInit const mptInitNoFund
Definition mpt.h:106
auto const data
General field definitions, or fields used in multiple transaction namespaces.
Json::Value noop(Account const &account)
The null transaction.
Definition noop.h:11
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:13
Json::Value signers(Account const &account, std::uint32_t quorum, std::vector< signer > const &v)
Definition multisign.cpp:15
std::uint32_t ownerCount(Env const &env, Account const &account)
XRPAmount txfee(Env const &env, std::uint16_t n)
XRP_t const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:95
std::unique_ptr< Config > makeConfig(std::map< std::string, std::string > extraTxQ={}, std::map< std::string, std::string > extraVoting={})
static Number number(STAmount const &a)
Definition AMM.cpp:17
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:11
Json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:101
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:72
static none_t const none
Definition tags.h:14
FeatureBitset testable_amendments()
Definition Env.h:78
auto const MPTDEXFlags
Definition mpt.h:16
Json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:10
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
@ telINSUF_FEE_P
Definition TER.h:37
@ telENV_RPC_FAILED
Definition TER.h:48
@ terNO_RIPPLE
Definition TER.h:204
@ terNO_ACCOUNT
Definition TER.h:197
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:97
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
int run(int argc, char **argv)
Definition Main.cpp:332
std::string to_string(base_uint< Bits, Tag > const &a)
Definition base_uint.h:602
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
static constexpr Number numZero
Definition Number.h:524
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:95
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:107
int getAssetsTotalScale(SLE::const_ref vaultSle)
@ tefBAD_SIGNATURE
Definition TER.h:159
@ tefBAD_AUTH
Definition TER.h:149
@ tefPAST_SEQ
Definition TER.h:155
@ tefNOT_MULTI_SIGNING
Definition TER.h:161
static constexpr std::uint32_t secondsInYear
LoanState computeTheoreticalLoanState(Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
std::size_t constexpr maxDataPayloadLength
The maximum length of Data payload.
Definition Protocol.h:238
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:437
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::rounding_mode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
@ current
This was a new validation and was added.
base_uint< 256 > uint256
Definition base_uint.h:531
TER checkLoanGuards(Asset const &vaultAsset, Number const &principalRequested, bool expectInterest, std::uint32_t paymentTotal, LoanProperties const &properties, beast::Journal j)
constexpr FlagValue tmfMPTSetCanTransfer
Definition TxFlags.h:366
constexpr FlagValue tmfMPTCanMutateCanTransfer
Definition TxFlags.h:346
constexpr FlagValue tmfMPTClearCanTransfer
Definition TxFlags.h:367
LedgerSpecificFlags
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:523
constexpr XRPAmount DROPS_PER_XRP
Number of drops per 1 XRP.
Definition XRPAmount.h:238
TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() *10)
constexpr Number abs(Number x) noexcept
Definition Number.h:719
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:695
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:436
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ txSign
inner transaction to sign
LoanState constructRoundedLoanState(SLE::const_ref loan)
@ temINVALID
Definition TER.h:90
@ temINVALID_FLAG
Definition TER.h:91
@ temDISABLED
Definition TER.h:94
@ temBAD_AMOUNT
Definition TER.h:69
@ temBAD_SIGNATURE
Definition TER.h:85
@ temINVALID_INNER_BATCH
Definition TER.h:123
@ temBAD_SIGNER
Definition TER.h:95
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
@ tecWRONG_ASSET
Definition TER.h:341
@ tecLOCKED
Definition TER.h:339
@ tecNO_ENTRY
Definition TER.h:287
@ tecNO_AUTH
Definition TER.h:281
@ tecTOO_SOON
Definition TER.h:299
@ tecFROZEN
Definition TER.h:284
@ tecINSUFFICIENT_FUNDS
Definition TER.h:306
@ tecEXPIRED
Definition TER.h:295
@ tecPRECISION_LOSS
Definition TER.h:344
@ tecINSUFFICIENT_RESERVE
Definition TER.h:288
@ tecKILLED
Definition TER.h:297
@ tecLIMIT_EXCEEDED
Definition TER.h:342
@ tecNO_PERMISSION
Definition TER.h:286
@ tecHAS_OBLIGATIONS
Definition TER.h:298
@ tecINSUFFICIENT_PAYMENT
Definition TER.h:308
LoanProperties computeLoanProperties(Asset const &asset, Number const &principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
@ tesSUCCESS
Definition TER.h:225
constexpr FlagValue tfFullyCanonicalSig
Definition TxFlags.h:40
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)
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, int depth=0)
Check if the account lacks required authorization for MPT.
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
T parse(T... args)
T reserve(T... args)
T size(T... args)
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:19
uint256 key
Definition Keylet.h:20
This structure captures the parts of a loan state.
Number principalOutstanding
int vaultScale(jtx::Env const &env) const
BrokerInfo(jtx::PrettyAsset const &asset_, Keylet const &brokerKeylet_, Keylet const &vaultKeylet_, BrokerParameters const &p)
Number maxCoveredLoanValue(Number const &currentDebt) const
Definition Loan_test.cpp:88
static BrokerParameters const & defaults()
Definition Loan_test.cpp:97
std::optional< TenthBips32 > lateInterest
std::optional< STAmount > setFee
std::optional< Number > lateFee
std::optional< std::uint32_t > payTotal
std::optional< std::uint32_t > payInterval
std::optional< TenthBips32 > interest
std::optional< Number > closeFee
jtx::JTx operator()(jtx::Env &env, BrokerInfo const &broker, FN const &... fN) const
std::optional< Number > serviceFee
std::optional< TenthBips32 > overpaymentInterest
std::optional< std::uint32_t > gracePd
std::optional< TenthBips32 > overFee
std::optional< TenthBips32 > closeInterest
std::optional< Number > originationFee
NetClock::time_point startDate
std::uint32_t const paymentInterval
std::optional< Number > overpaymentExtra
static PaymentParameters const & defaults()
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
VerifyLoanStatus(jtx::Env const &env_, BrokerInfo const &broker_, jtx::Account const &pseudo_, Keylet const &keylet_)
void checkPayment(std::int32_t loanScale, jtx::Account const &account, jtx::PrettyAmount const &balanceBefore, STAmount const &expectedPayment, jtx::PrettyAmount const &adjustment) const
void checkBroker(Number const &principalOutstanding, Number const &interestOwed, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, std::uint32_t ownerCount) const
Checks the expected broker state against the ledger.
void operator()(std::uint32_t previousPaymentDate, std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, Number const &loanScale, Number const &totalValue, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, std::uint32_t flags) const
Checks both the loan and broker expect states against the ledger.
void operator()(LoanState const &state) const
Checks both the loan and broker expect states against the ledger.
Execution context for applying a JSON transaction.
Definition JTx.h:25
std::shared_ptr< STTx const > stx
Definition JTx.h:35
std::optional< MPTCreate > create
Definition mpt.h:104
Represents an XRP or IOU quantity This customizes the string conversion and supports XRP conversions ...
STAmount const & value() const
Set the sequence number on a JTx.
Definition seq.h:14
T time(T... args)
T time_since_epoch(T... args)
T to_string(T... args)
T value_or(T... args)