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