xrpld
Loading...
Searching...
No Matches
LoanTestBase.h
1#pragma once
2
3#include <xrpl/beast/unit_test/suite.h>
4//
5#include <test/jtx/Account.h>
6#include <test/jtx/Env.h>
7#include <test/jtx/JTx.h>
8#include <test/jtx/TestHelpers.h>
9#include <test/jtx/amount.h>
10#include <test/jtx/fee.h>
11#include <test/jtx/flags.h>
12#include <test/jtx/mpt.h>
13#include <test/jtx/multisign.h>
14#include <test/jtx/pay.h>
15#include <test/jtx/sig.h>
16#include <test/jtx/tags.h>
17#include <test/jtx/ter.h>
18#include <test/jtx/trust.h>
19#include <test/jtx/vault.h>
20
21#include <xrpld/rpc/detail/Handler.h>
22
23#include <xrpl/basics/Number.h>
24#include <xrpl/basics/base_uint.h>
25#include <xrpl/basics/chrono.h>
26#include <xrpl/beast/utility/Journal.h>
27#include <xrpl/beast/utility/Zero.h>
28#include <xrpl/json/json_value.h>
29#include <xrpl/ledger/helpers/AccountRootHelpers.h>
30#include <xrpl/ledger/helpers/LendingHelpers.h>
31#include <xrpl/ledger/helpers/VaultHelpers.h>
32#include <xrpl/protocol/Asset.h>
33#include <xrpl/protocol/Feature.h>
34#include <xrpl/protocol/Indexes.h>
35#include <xrpl/protocol/Issue.h>
36#include <xrpl/protocol/Keylet.h>
37#include <xrpl/protocol/LedgerFormats.h>
38#include <xrpl/protocol/MPTIssue.h>
39#include <xrpl/protocol/Protocol.h>
40#include <xrpl/protocol/SField.h>
41#include <xrpl/protocol/STAmount.h>
42#include <xrpl/protocol/SeqProxy.h>
43#include <xrpl/protocol/TER.h>
44#include <xrpl/protocol/TxFlags.h>
45#include <xrpl/protocol/Units.h>
46#include <xrpl/protocol/XRPAmount.h>
47#include <xrpl/server/LoadFeeTrack.h>
48#include <xrpl/tx/transactors/lending/LoanSet.h>
49
50#include <algorithm>
51#include <array>
52#include <chrono>
53#include <cstddef>
54#include <cstdint>
55#include <cstdlib>
56#include <functional>
57#include <initializer_list>
58#include <optional>
59#include <ostream>
60#include <sstream>
61#include <stdexcept>
62#include <string>
63#include <tuple>
64#include <type_traits>
65#include <utility>
66#include <vector>
67
68namespace xrpl::test {
69
71{
72protected:
73 // Ensure that all the features needed for Lending Protocol are included,
74 // even if they are set to unsupported.
75 //
76 // featureLendingProtocolV1_1 is excluded from the default set: it changes
77 // Vault/LoanBroker accounting (AssetsTotal/DebtTotal/LossUnrealized), and
78 // most of this file's tests assert whole-life-specific expected values
79 // for those fields. Tests that specifically exercise the amendment opt
80 // it back in explicitly (e.g. `all_ | featureLendingProtocolV1_1`).
81 FeatureBitset const all_{jtx::testableAmendments() - featureLendingProtocolV1_1};
83
85 {
86 Number vaultDeposit = 1'000'000;
87 Number debtMax = 25'000;
89 int coverDeposit = 1000;
92 std::string data = {}; // NOLINT(readability-redundant-member-init)
94 // If set, the vault is created with this sfScale value. Useful for
95 // tests that need finer loanScale to exercise rounding edge cases.
97 std::nullopt; // NOLINT(readability-redundant-member-init)
98 // Vault kind axis. When ClosedEnded, createVaultAndBroker sets sfSubscriptionDate /
99 // sfRedemptionDate from env.now() using the offsets below and advances the ledger clock
100 // past SubscriptionDate so the vault is in the Investment phase by the time the broker is
101 // set up. Requires featureLendingProtocolV1_1.
103 // Seconds past env.now() at which SubscriptionDate lands. Must be strictly positive
104 // (VaultCreate::preclaim rejects SubscriptionDate <= parentCloseTime).
106 // Seconds between SubscriptionDate and RedemptionDate. Must be >= kMinInvestmentPeriod, <
107 // kMaxInvestmentPeriod, and generous enough to fit any loan schedule the test runs
108 // (finalPayment must be strictly before RedemptionDate). Default sized to comfortably
109 // exceed any schedule realistic tests are likely to configure.
110 std::uint32_t redemptionOffset = 10u * 365u * 24u * 60u * 60u;
111 // When true, createVaultAndBroker skips its automatic clock advance past SubscriptionDate.
112 // Useful for tests that need to observe the vault while it is still in the Subscription
113 // phase. Ignored for open-ended vaults.
114 bool skipPhaseAdvance = false;
115
116 [[nodiscard]] Number
117 maxCoveredLoanValue(Number const& currentDebt) const
118 {
120 auto debtLimit = coverDeposit * kTenthBipsPerUnity.value() / coverRateMin.value();
121
122 return debtLimit - currentDebt;
123 }
124
125 static BrokerParameters const&
127 {
128 static BrokerParameters const kResult{};
129 return kResult;
130 }
131
132 // TODO: create an operator() which returns a transaction similar to
133 // LoanParameters
134 };
135
137 {
142 // Absolute dates resolved by createVaultAndBroker when params.vaultKind
143 // is ClosedEnded; std::nullopt for open-ended vaults.
161
162 [[nodiscard]] Keylet
164 {
166 }
167 [[nodiscard]] Keylet
169 {
170 return keylet::vault(vaultID);
171 }
172
173 [[nodiscard]] int
174 vaultScale(jtx::Env const& env) const
175 {
176 using namespace jtx;
177
178 auto const vaultSle = env.le(keylet::vault(vaultID));
179 return getAssetsTotalScale(vaultSle);
180 }
181 };
182
184 {
185 // The account submitting the transaction. May be borrower or broker.
187 // The counterparty. Should be the other of borrower or broker.
189 // Whether the counterparty is specified in the `counterparty` field, or
190 // only signs.
193 // NOLINTBEGIN(readability-redundant-member-init)
208 // NOLINTEND(readability-redundant-member-init)
209
210 template <class... FN>
212 operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const
213 {
214 using namespace jtx;
215 using namespace jtx::loan;
216
217 JTx jt{loan::set(
218 account,
219 broker.brokerID,
220 broker.asset(principalRequest).number(),
221 flags.value_or(0))};
222
223 Sig(sfCounterpartySignature, counter)(env, jt);
224
225 Fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt);
226
228 kCounterparty(counter)(env, jt);
229 if (originationFee)
230 kLoanOriginationFee(broker.asset(*originationFee).number())(env, jt);
231 if (serviceFee)
232 kLoanServiceFee(broker.asset(*serviceFee).number())(env, jt);
233 if (lateFee)
234 kLatePaymentFee(broker.asset(*lateFee).number())(env, jt);
235 if (closeFee)
236 kClosePaymentFee(broker.asset(*closeFee).number())(env, jt);
237 if (overFee)
238 kOverpaymentFee (*overFee)(env, jt);
239 if (interest)
240 kInterestRate (*interest)(env, jt);
241 if (lateInterest)
242 kLateInterestRate (*lateInterest)(env, jt);
243 if (closeInterest)
244 kCloseInterestRate (*closeInterest)(env, jt);
246 kOverpaymentInterestRate (*overpaymentInterest)(env, jt);
247 if (payTotal)
248 kPaymentTotal (*payTotal)(env, jt);
249 if (payInterval)
250 kPaymentInterval (*payInterval)(env, jt);
251 if (gracePd)
252 kGracePeriod (*gracePd)(env, jt);
253
254 return env.jt(jt, fN...);
255 }
256 };
257
259 {
263 bool showStepBalances = false;
264 bool validateBalances = true;
265
266 static PaymentParameters const&
268 {
269 static PaymentParameters const kResult{};
270 return kResult;
271 }
272 };
273
289
295 {
296 public:
297 jtx::Env const& env;
301
303 jtx::Env const& env,
304 BrokerInfo const& broker,
305 jtx::Account const& pseudo,
306 Keylet const& keylet)
308 {
309 }
310
314 void
316 Number const& principalOutstanding,
317 Number const& interestOwed,
318 TenthBips32 interestRate,
319 std::uint32_t paymentInterval,
320 std::uint32_t paymentsRemaining,
322 {
323 using namespace jtx;
324 if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
325 env.test.BEAST_EXPECT(brokerSle))
326 {
327 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
328 auto const brokerDebt = brokerSle->at(sfDebtTotal);
329
330 if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
331 env.test.BEAST_EXPECT(vaultSle))
332 {
333 auto const expectedDebt =
334 env.current()->rules().enabled(featureLendingProtocolV1_1) &&
336 ? principalOutstanding
337 : principalOutstanding + interestOwed;
338 env.test.BEAST_EXPECT(brokerDebt == expectedDebt);
339 env.test.BEAST_EXPECT(
340 env.balance(pseudoAccount, broker.asset).number() ==
341 brokerSle->at(sfCoverAvailable));
342 env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount);
343
344 Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)};
345 env.test.BEAST_EXPECT(
346 vaultSle->at(sfAssetsAvailable) ==
347 env.balance(vaultPseudo, broker.asset).number());
348 if (ownerCount == 0)
349 {
350 // The Vault must be perfectly balanced if there
351 // are no loans outstanding
352 auto const total = vaultSle->at(sfAssetsTotal);
353 auto const available = vaultSle->at(sfAssetsAvailable);
354 env.test.BEAST_EXPECT(total == available);
355 env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
356 }
357 }
358 }
359 }
360
361 void
363 std::int32_t loanScale,
364 jtx::Account const& account,
365 jtx::PrettyAmount const& balanceBefore,
366 STAmount const& expectedPayment,
367 jtx::PrettyAmount const& adjustment) const
368 {
369 auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent());
370
371 STAmount const balanceChangeAmount{
372 broker.asset,
373 roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)};
374 {
375 auto const difference = roundToScale(
376 env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount),
377 borrowerScale);
378 env.test.expect(
379 roundToScale(difference, loanScale) >= beast::kZero,
380 "Balance before: " + to_string(balanceBefore.value()) +
381 ", expected change: " + to_string(balanceChangeAmount) +
382 ", difference (balance after - expected): " + to_string(difference),
383 __FILE__,
384 __LINE__);
385 }
386 }
387
391 void
393 std::uint32_t previousPaymentDate,
394 std::uint32_t nextPaymentDate,
395 std::uint32_t paymentRemaining,
396 Number const& loanScale,
397 Number const& totalValue,
398 Number const& principalOutstanding,
399 Number const& managementFeeOutstanding,
400 Number const& periodicPayment,
401 std::uint32_t flags) const
402 {
403 using namespace jtx;
404 if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
405 {
406 env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate);
407 env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining);
408 env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate);
409 env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale);
410 env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue);
411 env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding);
412 env.test.BEAST_EXPECT(
413 loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding);
414 env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment);
415 env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
416
417 auto const ls = constructLoanState(loan);
418
419 auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
420 auto const paymentInterval = loan->at(sfPaymentInterval);
422 principalOutstanding,
423 ls.interestDue,
424 interestRate,
425 paymentInterval,
426 paymentRemaining,
427 1);
428
429 if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
430 env.test.BEAST_EXPECT(brokerSle))
431 {
432 if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
433 env.test.BEAST_EXPECT(vaultSle))
434 {
435 if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u))
436 {
437 env.test.BEAST_EXPECT(
438 vaultSle->at(sfLossUnrealized) ==
439 (env.current()->rules().enabled(featureLendingProtocolV1_1) &&
441 ? principalOutstanding
442 : totalValue - managementFeeOutstanding));
443 }
444 else
445 {
446 env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
447 }
448 }
449 }
450 }
451 }
452
456 void
457 operator()(LoanState const& state) const
458 {
461 state.nextPaymentDate,
462 state.paymentRemaining,
463 state.loanScale,
464 state.totalValue,
467 state.periodicPayment,
468 state.flags);
469 };
470 };
471
472 BrokerInfo
474 jtx::Env& env,
475 jtx::PrettyAsset const& asset,
476 jtx::Account const& lender,
478 {
479 using namespace jtx;
480
481 Vault const vault{env};
482
483 auto const deposit = asset(params.vaultDeposit);
484 auto const debtMaximumValue = asset(params.debtMax).value();
485 auto const coverDepositValue = asset(params.coverDeposit).value();
486
487 auto const coverRateMinValue = params.coverRateMin;
488
489 std::optional<std::uint32_t> subscriptionDate;
490 std::optional<std::uint32_t> redemptionDate;
491 if (params.vaultKind == VaultKind::ClosedEnded)
492 {
493 auto const nowSec = env.now().time_since_epoch().count();
494 subscriptionDate = nowSec + params.subscriptionOffset;
495 redemptionDate = *subscriptionDate + params.redemptionOffset;
496 }
497
498 auto [tx, vaultKeylet] = vault.create(
499 {.owner = lender,
500 .asset = asset,
501 .vaultKind = params.vaultKind == VaultKind::OpenEnded
503 : std::optional<std::uint8_t>{std::to_underlying(params.vaultKind)},
504 .subscriptionDate = subscriptionDate,
505 .redemptionDate = redemptionDate});
506 if (params.vaultScale)
507 tx[sfScale] = *params.vaultScale;
508 env(tx);
509 env.close();
510 BEAST_EXPECT(env.le(vaultKeylet));
511
512 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
513 env.close();
514 if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault))
515 {
516 BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value());
517 }
518
519 // For closed-ended vaults, advance past SubscriptionDate so subsequent LoanSet operations
520 // run in the Investment phase (unless the caller explicitly asked to stay in Subscription).
521 if (subscriptionDate && !params.skipPhaseAdvance)
522 {
523 using d = NetClock::duration;
524 using tp = NetClock::time_point;
525 env.close(tp{d{*subscriptionDate + 1}});
526 }
527
528 auto const keylet = keylet::loanBroker(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
529
530 using namespace loan_broker;
531 env(set(lender, vaultKeylet.key, params.flags),
532 kData(params.data),
533 kManagementFeeRate(params.managementFeeRate),
534 kDebtMaximum(debtMaximumValue),
535 kCoverRateMinimum(coverRateMinValue),
536 kCoverRateLiquidation(TenthBips32(params.coverRateLiquidation)));
537
538 if (coverDepositValue != beast::kZero)
539 env(coverDeposit(lender, keylet.key, coverDepositValue));
540
541 env.close();
542
543 return {asset, keylet, vaultKeylet, params, subscriptionDate, redemptionDate};
544 }
545
550 getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet)
551 {
552 using d = NetClock::duration;
553 using tp = NetClock::time_point;
554
555 // Lookup the current loan state
556 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
557 {
558 return LoanState{
559 .previousPaymentDate = loan->at(sfPreviousPaymentDueDate),
560 .startDate = tp{d{loan->at(sfStartDate)}},
561 .nextPaymentDate = loan->at(sfNextPaymentDueDate),
562 .paymentRemaining = loan->at(sfPaymentRemaining),
563 .loanScale = loan->at(sfLoanScale),
564 .totalValue = loan->at(sfTotalValueOutstanding),
565 .principalOutstanding = loan->at(sfPrincipalOutstanding),
566 .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding),
567 .periodicPayment = loan->at(sfPeriodicPayment),
568 .flags = loan->at(sfFlags),
569 .paymentInterval = loan->at(sfPaymentInterval),
570 .interestRate = TenthBips32{loan->at(sfInterestRate)},
571 };
572 }
573 return LoanState{};
574 }
575
582 jtx::Env const& env,
583 BrokerInfo const& broker,
584 Keylet const& loanKeylet,
585 VerifyLoanStatus const& verifyLoanStatus)
586 {
587 using namespace std::chrono_literals;
588 using d = NetClock::duration;
589 using tp = NetClock::time_point;
590
591 auto const state = getCurrentState(env, broker, loanKeylet);
592 BEAST_EXPECT(state.previousPaymentDate == 0);
593 BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s);
594 BEAST_EXPECT(state.paymentRemaining == 12);
595 BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value());
596 BEAST_EXPECT(
597 state.loanScale >=
598 (broker.asset.integral()
599 ? 0
600 : std::max(broker.vaultScale(env), state.principalOutstanding.exponent())));
601 BEAST_EXPECT(state.paymentInterval == 600);
602 {
604 BEAST_EXPECT(
605 state.totalValue ==
607 broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale));
608 }
609 BEAST_EXPECT(
610 state.managementFeeOutstanding ==
612 broker.asset,
613 state.totalValue - state.principalOutstanding,
615 state.loanScale));
616
617 verifyLoanStatus(state);
618
619 return state;
620 }
621
622 bool
623 canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state)
624 {
625 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
626 BEAST_EXPECT(brokerSle))
627 {
628 if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
629 BEAST_EXPECT(vaultSle))
630 {
631 // log << vaultSle->getJson() << std::endl;
632 auto const assetsUnavailable =
633 vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable);
634 auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) +
635 (env.current()->rules().enabled(featureLendingProtocolV1_1) &&
638 : state.totalValue - state.managementFeeOutstanding);
639
640 if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable))
641 {
642 return false;
643 }
644 }
645 }
646 return true;
647 }
648
649 enum class AssetType { XRP = 0, IOU = 1, MPT = 2 };
650
651 // Specify the accounts as params to allow other accounts to be used
654 jtx::Env& env,
655 AssetType assetType,
656 BrokerParameters const& brokerParams,
657 jtx::Account const& issuer,
658 jtx::Account const& lender,
659 jtx::Account const& borrower)
660 {
661 using namespace jtx;
662
663 switch (assetType)
664 {
665 case AssetType::XRP:
666 // TODO: remove the factor, and set up loans in drops
667 return PrettyAsset{xrpIssue(), 1'000'000};
668
669 case AssetType::IOU: {
670 PrettyAsset const asset{issuer[iouCurrency_]};
671
672 auto const limit =
673 asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit));
674 if (lender != issuer)
675 env(trust(lender, limit));
676 if (borrower != issuer)
677 env(trust(borrower, limit));
678
679 return asset;
680 }
681
682 case AssetType::MPT: {
683 // Enough to cover initial fees
684 if (!env.le(keylet::account(issuer)))
685 env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer);
686 if (!env.le(keylet::account(lender)))
687 env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender));
688 if (!env.le(keylet::account(borrower)))
689 env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower));
690
691 MPTTester mptt{env, issuer, kMptInitNoFund};
692 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
693 // Scale the MPT asset so interest is interesting
694 PrettyAsset const asset{mptt.issuanceID(), 10'000};
695 // Need to do the authorization here because mptt isn't
696 // accessible outside
697 if (lender != issuer)
698 mptt.authorize({.account = lender});
699 if (borrower != issuer)
700 mptt.authorize({.account = borrower});
701
702 env.close();
703
704 return asset;
705 }
706
707 default:
708 throw std::runtime_error("Unknown asset type");
709 }
710 }
711
712 // Predicts the keylet of the next loan `broker` will originate, before
713 // that loan exists, by reading the broker's current LoanSequence.
714 Keylet
715 nextLoanKeylet(jtx::Env const& env, BrokerInfo const& broker)
716 {
717 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
718 if (!BEAST_EXPECT(brokerStateBefore))
720 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
721 return keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
722 }
723
724 // Funds issuer/lender/borrower with XRP, creates an IOU asset issued by
725 // `issuer`, establishes trustlines for lender and borrower, and pays
726 // them starting balances. This is the exact setup shared by several of
727 // the fuzzer-derived regression tests below.
730 jtx::Env& env,
731 jtx::Account const& issuer,
732 jtx::Account const& lender,
733 jtx::Account const& borrower,
734 Number const& lenderPay = 100'000'000,
735 Number const& borrowerPay = 1'000'000)
736 {
737 using namespace jtx;
738
739 env.fund(XRP(1'000'000), issuer, lender, borrower);
740 env.close();
741
742 PrettyAsset const iouAsset = issuer[iouCurrency_];
743 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
744 env(trustLenderTx);
745 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
746 env(trustBorrowerTx);
747 auto payLenderTx = pay(issuer, lender, iouAsset(lenderPay));
748 env(payLenderTx);
749 auto payIssuerTx = pay(issuer, borrower, iouAsset(borrowerPay));
750 env(payIssuerTx);
751 env.close();
752
753 return iouAsset;
754 }
755
756 // Funds issuer/lender/borrower with XRP, sets DefaultRipple on the
757 // issuer, creates a "USD" IOU asset with a large trust limit, and pays
758 // lender/borrower starting balances. Shared setup for several
759 // overpayment/rounding regression tests below.
760 static jtx::PrettyAsset
762 jtx::Env& env,
763 jtx::Account const& issuer,
764 jtx::Account const& lender,
765 jtx::Account const& borrower,
766 Number const& lenderPay = 1'000'000,
767 Number const& borrowerPay = 1'000'000)
768 {
769 using namespace jtx;
770
771 env.fund(XRP(1'000'000), issuer, lender, borrower);
772 env(fset(issuer, asfDefaultRipple));
773 env.close();
774
775 PrettyAsset const iouAsset = issuer["USD"];
776 STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}};
777 env(trust(lender, iouLimit));
778 env(trust(borrower, iouLimit));
779 env(pay(issuer, lender, iouAsset(lenderPay)));
780 env(pay(issuer, borrower, iouAsset(borrowerPay)));
781 env.close();
782
783 return iouAsset;
784 }
785
786 // Returns the broker's pseudo-account, or `fallback` if the broker's
787 // ledger entry cannot be read.
789 brokerPseudoAccount(jtx::Env const& env, BrokerInfo const& broker, jtx::Account const& fallback)
790 {
791 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
792 if (!BEAST_EXPECT(brokerSle))
793 return fallback;
794 auto const brokerPseudo = brokerSle->at(sfAccount);
795 return jtx::Account("Broker pseudo-account", brokerPseudo);
796 }
797
798 void
800 jtx::Env& env,
801 BrokerParameters const& brokerParams,
802 LoanParameters const& loanParams,
803 AssetType assetType,
804 jtx::Account const& issuer,
805 jtx::Account const& lender,
806 jtx::Account const& borrower)
807 {
808 using namespace jtx;
809
810 auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower);
811 auto const principal = asset(loanParams.principalRequest).number();
812 auto const interest = loanParams.interest.value_or(TenthBips32{});
813 auto const interval = loanParams.payInterval.value_or(LoanSet::kDefaultPaymentInterval);
814 auto const total = loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal);
815 auto const feeRate = brokerParams.managementFeeRate;
816 auto const props = computeLoanProperties(
817 env.current()->rules(),
818 asset,
819 principal,
820 interest,
821 interval,
822 total,
823 feeRate,
824 asset(brokerParams.vaultDeposit).number().exponent());
825 log << "Loan properties:\n"
826 << "\tPrincipal: " << principal << std::endl
827 << "\tInterest rate: " << interest << std::endl
828 << "\tPayment interval: " << interval << std::endl
829 << "\tManagement Fee Rate: " << feeRate << std::endl
830 << "\tTotal Payments: " << total << std::endl
831 << "\tPeriodic Payment: " << props.periodicPayment << std::endl
832 << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl
833 << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl
834 << "\tLoan Scale: " << props.loanScale << std::endl
835 << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl;
836
837 // checkGuards returns a TER, so success is 0
838 BEAST_EXPECT(!checkLoanGuards(
839 asset,
840 asset(loanParams.principalRequest).number(),
841 loanParams.interest.value_or(TenthBips32{}) != beast::kZero,
843 props,
844 env.journal));
845 }
846
849 jtx::Env& env,
850 AssetType assetType,
851 BrokerParameters const& brokerParams,
852 LoanParameters const& loanParams,
853 jtx::Account const& issuer,
854 jtx::Account const& lender,
855 jtx::Account const& borrower)
856 {
857 using namespace jtx;
858
859 // Enough to cover initial fees
860 env.fund(env.current()->fees().accountReserve(10, 1) * 10, issuer);
861 if (lender != issuer)
862 env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(lender));
863 if (borrower != issuer && borrower != lender)
864 env.fund(env.current()->fees().accountReserve(10, 1) * 10, noripple(borrower));
865
866 describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower);
867
868 // Make the asset
869 auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower);
870
871 env.close();
872 if (asset.native() || lender != issuer)
873 {
874 env(
875 pay((asset.native() ? env.master : issuer),
876 lender,
877 asset(brokerParams.vaultDeposit + brokerParams.coverDeposit)));
878 }
879 // Fund the borrower later once we know the total loan
880 // size
881
882 BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams);
883
884 auto const pseudoAcctOpt = [&]() -> std::optional<Account> {
885 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
886 if (!BEAST_EXPECT(brokerSle))
887 return std::nullopt;
888 auto const brokerPseudo = brokerSle->at(sfAccount);
889 return Account("Broker pseudo-account", brokerPseudo);
890 }();
891 if (!pseudoAcctOpt)
892 return std::nullopt;
893 Account const& pseudoAcct = *pseudoAcctOpt;
894
895 auto const loanKeyletOpt = [&]() -> std::optional<Keylet> {
896 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
897 if (!BEAST_EXPECT(brokerSle))
898 return std::nullopt;
899
900 // Broker has no loans
901 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
902
903 // The loan keylet is based on the LoanSequence of the
904 // _LOAN_BROKER_ object.
905 auto const loanSequence = brokerSle->at(sfLoanSequence);
906 return keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence));
907 }();
908 if (!loanKeyletOpt)
909 return std::nullopt;
910 Keylet const& loanKeylet = *loanKeyletOpt;
911
912 env(loanParams(env, broker));
913
914 env.close();
915
916 return std::make_tuple(broker, loanKeylet, pseudoAcct);
917 }
918
919 static void
921 jtx::Env& env,
922 BrokerInfo const& broker,
923 jtx::Account const& issuer,
924 jtx::Account const& borrower,
925 LoanState const& state,
926 std::optional<Number> const& servFee)
927 {
928 using namespace jtx;
929
930 STAmount const serviceFee = broker.asset(servFee.value_or(0));
931
932 // Ensure the borrower has enough funds to make the payments
933 // (including tx fees, if necessary)
934 auto const borrowerBalance = env.balance(borrower, broker.asset);
935
936 auto const baseFee = env.current()->fees().base;
937
938 // Add extra for transaction fees and reserves, if appropriate, or a
939 // tiny amount for the extra paid in each transaction
940 auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) +
941 (broker.asset.native() ? Number(
942 baseFee * state.paymentRemaining +
943 accountReserve(*env.current(), borrower.id(), env.journal))
944 : broker.asset(15).number());
945
946 auto const shortage = totalNeeded - borrowerBalance.number();
947
948 if (shortage > beast::kZero && (broker.asset.native() || issuer != borrower))
949 {
950 env(
951 pay((broker.asset.native() ? env.master : issuer),
952 borrower,
953 STAmount{broker.asset, shortage}));
954 }
955 }
956
957 void
959 jtx::Env& env,
960 BrokerInfo const& broker,
961 LoanParameters const& loanParams,
962 Keylet const& loanKeylet,
963 VerifyLoanStatus const& verifyLoanStatus,
964 jtx::Account const& issuer,
965 jtx::Account const& lender,
966 jtx::Account const& borrower,
967 PaymentParameters const& paymentParams = PaymentParameters::defaults())
968 {
969 // Make all the individual payments
970 using namespace jtx;
971 using namespace jtx::loan;
972 using namespace std::chrono_literals;
973 using d = NetClock::duration;
974
975 bool const showStepBalances = paymentParams.showStepBalances;
976
977 auto const currencyLabel = getCurrencyLabel(broker.asset);
978
979 auto const baseFee = env.current()->fees().base;
980
981 env.close();
982 auto state = getCurrentState(env, broker, loanKeylet);
983
984 verifyLoanStatus(state);
985
986 STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0));
987
988 topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee);
989
990 // Periodic payment amount will consist of
991 // 1. principal outstanding (1000)
992 // 2. interest interest rate (at 12%)
993 // 3. payment interval (600s)
994 // 4. loan service fee (2)
995 // Calculate these values without the helper functions
996 // to verify they're working correctly The numbers in
997 // the below BEAST_EXPECTs may not hold across assets.
998 auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval);
999 STAmount const roundedPeriodicPayment{
1000 broker.asset,
1001 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
1002
1003 if (!showStepBalances)
1004 {
1005 log << currencyLabel << " Payment components: "
1006 << "Payments remaining, "
1007 << "rawInterest, rawPrincipal, "
1008 "rawMFee, "
1009 << "trackedValueDelta, trackedPrincipalDelta, "
1010 "trackedInterestDelta, trackedMgmtFeeDelta, special"
1011 << std::endl;
1012 }
1013
1014 // Include the service fee
1015 STAmount const totalDue = roundToScale(
1016 roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward);
1017
1018 auto currentRoundedState = constructLoanState(
1019 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
1020 {
1021 auto const raw = computeTheoreticalLoanState(
1022 env.current()->rules(),
1023 state.periodicPayment,
1024 periodicRate,
1025 state.paymentRemaining,
1026 broker.params.managementFeeRate);
1027
1028 if (showStepBalances)
1029 {
1030 log << currencyLabel << " Starting loan balances: "
1031 << "\n\tTotal value: " << currentRoundedState.valueOutstanding
1032 << "\n\tPrincipal: " << currentRoundedState.principalOutstanding
1033 << "\n\tInterest: " << currentRoundedState.interestDue
1034 << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue
1035 << "\n\tPayments remaining " << state.paymentRemaining << std::endl;
1036 }
1037 else
1038 {
1039 log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", "
1040 << raw.interestDue << ", " << raw.principalOutstanding << ", "
1041 << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", "
1042 << currentRoundedState.principalOutstanding << ", "
1043 << currentRoundedState.interestDue << ", "
1044 << currentRoundedState.managementFeeDue << std::endl;
1045 }
1046 }
1047
1048 // Try to pay a little extra to show that it's _not_
1049 // taken
1050 auto const extraAmount = paymentParams.overpaymentExtra
1051 ? broker.asset(*paymentParams.overpaymentExtra).value()
1052 : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20});
1053
1054 STAmount const transactionAmount =
1055 STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount;
1056
1057 auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number();
1058 auto const initialState = state;
1060 .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0};
1061 Number totalInterestPaid = 0;
1062 Number totalFeesPaid = 0;
1063 std::size_t totalPaymentsMade = 0;
1064
1066 env.current()->rules(),
1067 state.periodicPayment,
1068 periodicRate,
1069 state.paymentRemaining,
1070 broker.params.managementFeeRate);
1071
1072 auto validateBorrowerBalance = [&]() {
1073 if (borrower == issuer || !paymentParams.validateBalances)
1074 return;
1075 auto const totalSpent =
1076 (totalPaid.trackedValueDelta + totalFeesPaid +
1077 (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : kNumZero));
1078 BEAST_EXPECT(
1079 env.balance(borrower, broker.asset).number() ==
1080 borrowerInitialBalance - totalSpent);
1081 };
1082
1083 auto const defaultRound = broker.asset.integral() ? 3 : 0;
1084 auto truncate = [defaultRound](Number const& n, std::optional<int> places = std::nullopt) {
1085 auto const p = places.value_or(defaultRound);
1086 if (p == 0)
1087 return n;
1088 auto const factor = Number{1, p};
1089 return (n * factor).truncate() / factor;
1090 };
1091 while (state.paymentRemaining > 0)
1092 {
1093 validateBorrowerBalance();
1094 // Compute the expected principal amount
1095 auto const paymentComponents = xrpl::detail::computePaymentComponents(
1096 env.current()->rules(),
1097 broker.asset.raw(),
1098 state.loanScale,
1099 state.totalValue,
1100 state.principalOutstanding,
1101 state.managementFeeOutstanding,
1102 state.periodicPayment,
1103 periodicRate,
1104 state.paymentRemaining,
1105 broker.params.managementFeeRate);
1106
1107 BEAST_EXPECT(
1108 paymentComponents.trackedValueDelta <= roundedPeriodicPayment ||
1109 (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final &&
1110 paymentComponents.trackedValueDelta >= roundedPeriodicPayment));
1111 BEAST_EXPECT(
1112 paymentComponents.trackedValueDelta ==
1113 paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() +
1114 paymentComponents.trackedManagementFeeDelta);
1115
1116 xrpl::LoanState const nextTrueState = computeTheoreticalLoanState(
1117 env.current()->rules(),
1118 state.periodicPayment,
1119 periodicRate,
1120 state.paymentRemaining - 1,
1121 broker.params.managementFeeRate);
1122 xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState;
1123 BEAST_EXPECT(
1124 deltas.total() == deltas.principal + deltas.interest + deltas.managementFee);
1125 BEAST_EXPECT(
1126 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
1127 deltas.total() == state.periodicPayment ||
1128 (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14);
1129
1130 if (!showStepBalances)
1131 {
1132 log << currencyLabel << " Payment components: " << state.paymentRemaining << ", "
1133
1134 << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee
1135 << ", " << paymentComponents.trackedValueDelta << ", "
1136 << paymentComponents.trackedPrincipalDelta << ", "
1137 << paymentComponents.trackedInterestPart() << ", "
1138 << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* {
1139 if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Final)
1140 return "final";
1141 if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Extra)
1142 return "extra";
1143 return "none";
1144 }() << std::endl;
1145 }
1146
1147 auto const totalDueAmount =
1148 STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee};
1149
1150 if (paymentParams.validateBalances)
1151 {
1152 // Due to the rounding algorithms to keep the interest and
1153 // principal in sync with "true" values, the computed amount
1154 // may be a little less than the rounded fixed payment
1155 // amount. For integral types, the difference should be < 3
1156 // (1 unit for each of the interest and management fee). For
1157 // IOUs, the difference should be dust.
1158 Number const diff = totalDue - totalDueAmount;
1159 BEAST_EXPECT(
1160 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
1161 diff == beast::kZero ||
1162 (diff > beast::kZero &&
1163 ((broker.asset.integral() && (static_cast<Number>(diff) < 3)) ||
1164 (state.loanScale - diff.exponent() > 13))));
1165
1166 BEAST_EXPECT(
1167 paymentComponents.trackedPrincipalDelta >= beast::kZero &&
1168 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
1169 BEAST_EXPECT(
1170 paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final ||
1171 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
1172 }
1173
1174 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
1175
1176 // Make the payment
1177 env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags));
1178
1179 env.close(d{state.paymentInterval / 2});
1180
1181 if (paymentParams.validateBalances)
1182 {
1183 // Need to account for fees if the loan is in XRP
1184 PrettyAmount adjustment = broker.asset(0);
1185 if (broker.asset.native())
1186 {
1187 adjustment = env.current()->fees().base;
1188 }
1189
1190 // Check the result
1191 verifyLoanStatus.checkPayment(
1192 state.loanScale,
1193 borrower,
1194 borrowerBalanceBeforePayment,
1195 totalDueAmount,
1196 adjustment);
1197 }
1198
1199 if (showStepBalances)
1200 {
1201 auto const loanSle = env.le(loanKeylet);
1202 if (!BEAST_EXPECT(loanSle))
1203 {
1204 // No reason for this not to exist
1205 return;
1206 }
1207 auto const current = constructLoanState(loanSle);
1208 auto const errors = nextTrueState - current;
1209 log << currencyLabel << " Loan balances: "
1210 << "\n\tAmount taken: " << paymentComponents.trackedValueDelta
1211 << "\n\tTotal value: " << current.valueOutstanding
1212 << " (true: " << truncate(nextTrueState.valueOutstanding)
1213 << ", error: " << truncate(errors.total())
1214 << ")\n\tPrincipal: " << current.principalOutstanding
1215 << " (true: " << truncate(nextTrueState.principalOutstanding)
1216 << ", error: " << truncate(errors.principal)
1217 << ")\n\tInterest: " << current.interestDue
1218 << " (true: " << truncate(nextTrueState.interestDue)
1219 << ", error: " << truncate(errors.interest)
1220 << ")\n\tMgmt fee: " << current.managementFeeDue
1221 << " (true: " << truncate(nextTrueState.managementFeeDue)
1222 << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining "
1223 << loanSle->at(sfPaymentRemaining) << std::endl;
1224
1225 currentRoundedState = current;
1226 }
1227
1228 --state.paymentRemaining;
1229 state.previousPaymentDate = state.nextPaymentDate;
1230 if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final)
1231 {
1232 state.paymentRemaining = 0;
1233 state.nextPaymentDate = 0;
1234 }
1235 else
1236 {
1237 state.nextPaymentDate += state.paymentInterval;
1238 }
1239 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
1240 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
1241 state.totalValue -= paymentComponents.trackedValueDelta;
1242
1243 if (paymentParams.validateBalances)
1244 verifyLoanStatus(state);
1245
1246 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
1247 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
1248 totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta;
1249 totalInterestPaid += paymentComponents.trackedInterestPart();
1250 totalFeesPaid += serviceFee;
1251 ++totalPaymentsMade;
1252
1253 currentTrueState = nextTrueState;
1254 }
1255 validateBorrowerBalance();
1256
1257 // Loan is paid off
1258 BEAST_EXPECT(state.paymentRemaining == 0);
1259 BEAST_EXPECT(state.principalOutstanding == 0);
1260
1261 auto const initialInterestDue = initialState.totalValue -
1262 (initialState.principalOutstanding + initialState.managementFeeOutstanding);
1263 if (paymentParams.validateBalances)
1264 {
1265 // Make sure all the payments add up
1266 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
1267 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
1268 BEAST_EXPECT(
1269 totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
1270 // This is almost a tautology given the previous checks, but
1271 // check it anyway for completeness.
1272 BEAST_EXPECT(totalInterestPaid == initialInterestDue);
1273 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
1274 }
1275
1276 if (showStepBalances)
1277 {
1278 auto const loanSle = env.le(loanKeylet);
1279 if (!BEAST_EXPECT(loanSle))
1280 {
1281 // No reason for this not to exist
1282 return;
1283 }
1284 log << currencyLabel << " Total amounts paid: "
1285 << "\n\tTotal value: " << totalPaid.trackedValueDelta
1286 << " (initial: " << truncate(initialState.totalValue)
1287 << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta)
1288 << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta
1289 << " (initial: " << truncate(initialState.principalOutstanding) << ", error: "
1290 << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta)
1291 << ")\n\tInterest: " << totalInterestPaid
1292 << " (initial: " << truncate(initialInterestDue)
1293 << ", error: " << truncate(initialInterestDue - totalInterestPaid)
1294 << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta
1295 << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: "
1296 << truncate(
1297 initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta)
1298 << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl;
1299 }
1300 }
1301
1302 void
1304 AssetType assetType,
1305 BrokerParameters const& brokerParams,
1306 LoanParameters const& loanParams,
1307 FeatureBitset features)
1308 {
1309 using namespace jtx;
1310
1311 Account const issuer("issuer");
1312 Account const lender("lender");
1313 Account const borrower("borrower");
1314
1315 Env env(*this, features);
1316
1317 auto loanResult =
1318 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
1319 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
1320 return;
1321
1322 auto broker = std::get<BrokerInfo>(*loanResult);
1323 auto loanKeylet = std::get<Keylet>(*loanResult);
1324 auto pseudoAcct = std::get<Account>(*loanResult);
1325
1326 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
1327
1329 env,
1330 broker,
1331 loanParams,
1332 loanKeylet,
1333 verifyLoanStatus,
1334 issuer,
1335 lender,
1336 borrower,
1337 PaymentParameters{.showStepBalances = true});
1338 }
1339
1351 void
1353 std::string const& caseLabel,
1354 char const* label,
1355 jtx::Env& env,
1356 Number const& loanAmount,
1357 int interestExponent,
1358 jtx::Account const& lender,
1359 jtx::Account const& borrower,
1360 jtx::Account const& evan,
1361 BrokerInfo const& broker,
1362 jtx::Account const& pseudoAcct,
1363 std::uint32_t flags,
1364 // The end of life callback is expected to take the loan to 0 payments
1365 // remaining, one way or another
1366 std::function<void(Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus)>
1367 toEndOfLife)
1368 {
1369 auto const [keylet, loanSequence] = [&]() {
1370 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1371 if (!BEAST_EXPECT(brokerSle))
1372 {
1373 // will be invalid
1375 }
1376
1377 // Broker has no loans
1378 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1379
1380 // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_
1381 // object.
1382 auto const loanSequence = brokerSle->at(sfLoanSequence);
1383 return std::make_pair(
1384 keylet::loan(broker.brokerID, SeqProxy::rawSequence(loanSequence)), loanSequence);
1385 }();
1386
1387 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet);
1388
1389 // No loans yet
1390 verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0);
1391
1392 if (!BEAST_EXPECT(loanSequence != 0))
1393 return;
1394
1395 testcase << caseLabel << " " << label;
1396
1397 using namespace jtx;
1398 using namespace loan;
1399 using namespace std::chrono_literals;
1400
1401 auto applyExponent = [interestExponent, this](TenthBips32 value) mutable {
1402 BEAST_EXPECT(value > TenthBips32(0));
1403 while (interestExponent > 0)
1404 {
1405 auto const oldValue = value;
1406 value *= 10;
1407 --interestExponent;
1408 BEAST_EXPECT(value / 10 == oldValue);
1409 }
1410 while (interestExponent < 0)
1411 {
1412 auto const oldValue = value;
1413 value /= 10;
1414 ++interestExponent;
1415 BEAST_EXPECT(value * 10 == oldValue);
1416 }
1417 return value;
1418 };
1419
1420 auto const borrowerOwnerCount = env.ownerCount(borrower);
1421
1422 auto const loanSetFee = env.current()->fees().base * 2;
1423 LoanParameters const loanParams{
1424 .account = borrower,
1425 .counter = lender,
1426 .counterpartyExplicit = false,
1427 .principalRequest = loanAmount,
1428 .setFee = loanSetFee,
1429 .originationFee = 1,
1430 .serviceFee = 2,
1431 .lateFee = 3,
1432 .closeFee = 4,
1433 .overFee = applyExponent(percentageToTenthBips(5) / 10),
1434 .interest = applyExponent(percentageToTenthBips(12)),
1435 // 2.4%
1436 .lateInterest = applyExponent(percentageToTenthBips(24) / 10),
1437 .closeInterest = applyExponent(percentageToTenthBips(36) / 10),
1438 .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10),
1439 .payTotal = 12,
1440 .payInterval = 600,
1441 .gracePd = 60,
1442 .flags = flags,
1443 };
1444 Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value();
1445 auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value();
1446 auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value();
1447 auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value();
1448 auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value();
1449
1450 auto const borrowerStartbalance = env.balance(borrower, broker.asset);
1451
1452 auto createJtx = loanParams(env, broker);
1453 // Successfully create a Loan
1454 env(createJtx);
1455
1456 env.close();
1457
1458 auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count();
1459
1460 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1461 BEAST_EXPECT(brokerSle))
1462 {
1463 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1);
1464 }
1465
1466 {
1467 // Need to account for fees if the loan is in XRP
1468 PrettyAmount adjustment = broker.asset(0);
1469 if (broker.asset.native())
1470 {
1471 adjustment = 2 * env.current()->fees().base;
1472 }
1473
1474 BEAST_EXPECT(
1475 env.balance(borrower, broker.asset).value() ==
1476 borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount -
1477 adjustment.value());
1478 }
1479
1480 auto const loanFlags =
1481 createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0);
1482
1483 if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
1484 {
1485 // log << "loan after create: " << to_string(loan->getJson())
1486 // << std::endl;
1487 BEAST_EXPECT(
1488 loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment));
1489 BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence);
1490 BEAST_EXPECT(loan->at(sfBorrower) == borrower.id());
1491 BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID);
1492 BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount);
1493 BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount);
1494 BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount);
1495 BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount);
1496 BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee);
1497 BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest);
1498 BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest);
1499 BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest);
1500 BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest);
1501 BEAST_EXPECT(loan->at(sfStartDate) == startDate);
1502 BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval);
1503 BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd);
1504 BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0);
1505 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval);
1506 BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal);
1507 BEAST_EXPECT(
1508 loan->at(sfLoanScale) >=
1509 (broker.asset.integral()
1510 ? 0
1511 : std::max(broker.vaultScale(env), principalRequestAmount.exponent())));
1512 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount);
1513 }
1514
1515 auto state = getCurrentState(env, broker, keylet, verifyLoanStatus);
1516
1517 auto const loanProperties = computeLoanProperties(
1518 env.current()->rules(),
1519 broker.asset.raw(),
1520 state.principalOutstanding,
1521 state.interestRate,
1522 state.paymentInterval,
1523 state.paymentRemaining,
1525 state.loanScale);
1526
1527 verifyLoanStatus(
1528 0,
1529 startDate + *loanParams.payInterval,
1530 *loanParams.payTotal,
1531 state.loanScale,
1532 loanProperties.loanState.valueOutstanding,
1533 principalRequestAmount,
1534 loanProperties.loanState.managementFeeDue,
1535 loanProperties.periodicPayment,
1536 loanFlags | 0);
1537
1538 // Manage the loan
1539 // no-op
1540 env(manage(lender, keylet.key, 0));
1541 {
1542 // no flags
1543 auto jt = manage(lender, keylet.key, 0);
1544 jt.removeMember(sfFlags.getName());
1545 env(jt);
1546 }
1547 // Only the lender can manage
1548 env(manage(evan, keylet.key, 0), Ter(tecNO_PERMISSION));
1549 // unknown flags
1550 env(manage(lender, keylet.key, tfLoanManageMask), Ter(temINVALID_FLAG));
1551 // combinations of flags are not allowed
1552 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), Ter(temINVALID_FLAG));
1553 env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), Ter(temINVALID_FLAG));
1554 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), Ter(temINVALID_FLAG));
1555 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault),
1557 // invalid loan ID
1558 env(manage(lender, broker.brokerID, tfLoanImpair), Ter(tecNO_ENTRY));
1559 // Loan is unimpaired, can't unimpair it again
1560 env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION));
1561 // Loan is unimpaired, it can go into default, but only after it's past
1562 // due
1563 env(manage(lender, keylet.key, tfLoanDefault), Ter(tecTOO_SOON));
1564
1565 // Check the vault
1566 bool const canImpair = canImpairLoan(env, broker, state);
1567 // Impair the loan, if possible
1568 env(manage(lender, keylet.key, tfLoanImpair),
1569 canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
1570 // Unimpair the loan
1571 env(manage(lender, keylet.key, tfLoanUnimpair),
1572 canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION));
1573
1574 auto const nextDueDate = startDate + *loanParams.payInterval;
1575
1576 env.close();
1577
1578 verifyLoanStatus(
1579 0,
1580 nextDueDate,
1581 *loanParams.payTotal,
1582 loanProperties.loanScale,
1583 loanProperties.loanState.valueOutstanding,
1584 principalRequestAmount,
1585 loanProperties.loanState.managementFeeDue,
1586 loanProperties.periodicPayment,
1587 loanFlags | 0);
1588
1589 // Can't delete the loan yet. It has payments remaining.
1590 env(del(lender, keylet.key), Ter(tecHAS_OBLIGATIONS));
1591
1592 if (BEAST_EXPECT(toEndOfLife))
1593 toEndOfLife(keylet, verifyLoanStatus);
1594 env.close();
1595
1596 // Verify the loan is at EOL
1597 if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
1598 {
1599 BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
1600 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
1601 }
1602 auto const borrowerStartingBalance = env.balance(borrower, broker.asset);
1603
1604 // Try to delete the loan broker with an active loan
1605 env(loan_broker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS));
1606 // Ensure the above tx doesn't get ordered after the LoanDelete and
1607 // delete our broker!
1608 env.close();
1609
1610 // Test failure cases
1611 env(del(lender, keylet.key, tfLoanOverpayment), Ter(temINVALID_FLAG));
1612 env(del(evan, keylet.key), Ter(tecNO_PERMISSION));
1613 env(del(lender, broker.brokerID), Ter(tecNO_ENTRY));
1614
1615 // Delete the loan
1616 // Either the borrower or the lender can delete the loan. Alternate
1617 // between who does it across tests.
1618 static unsigned kDeleteCounter = 0;
1619 auto const deleter = ((++kDeleteCounter % 2) != 0u) ? lender : borrower;
1620 env(del(deleter, keylet.key));
1621 env.close();
1622
1623 PrettyAmount adjustment = broker.asset(0);
1624 if (deleter == borrower)
1625 {
1626 // Need to account for fees if the loan is in XRP
1627 if (broker.asset.native())
1628 {
1629 adjustment = env.current()->fees().base;
1630 }
1631 }
1632
1633 // No loans left
1634 verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0);
1635
1636 BEAST_EXPECT(
1637 env.balance(borrower, broker.asset).value() ==
1638 borrowerStartingBalance.value() - adjustment);
1639 BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
1640
1641 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1642 BEAST_EXPECT(brokerSle))
1643 {
1644 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1645 }
1646 }
1647
1648 static std::string
1650 {
1651 if (asset.native())
1652 return "XRP";
1653 if (asset.holds<Issue>())
1654 return "IOU";
1655 if (asset.holds<MPTIssue>())
1656 return "MPT";
1657 return "Unknown";
1658 }
1659
1669 template <class TAsset, std::size_t NAsset>
1670 void
1672 jtx::Env& env,
1673 jtx::MPTTester& mptt,
1674 std::array<TAsset, NAsset> const& assets,
1675 BrokerInfo const& broker,
1676 Number const& loanAmount,
1677 int interestExponent)
1678 {
1679 using namespace jtx;
1680 using namespace lending;
1681
1682 auto const& asset = broker.asset.raw();
1683 auto const currencyLabel = getCurrencyLabel(asset);
1684 auto const caseLabel = [&]() {
1686 ss << "Lifecycle: " << loanAmount << " " << currencyLabel
1687 << " Scale interest to: " << interestExponent << " ";
1688 return ss.str();
1689 }();
1690 testcase << caseLabel;
1691
1692 using namespace loan;
1693 using namespace std::chrono_literals;
1694 using d = NetClock::duration;
1695 using tp = NetClock::time_point;
1696
1697 Account const issuer{"issuer"};
1698 // For simplicity, lender will be the sole actor for the vault &
1699 // brokers.
1700 Account const lender{"lender"};
1701 // Borrower only wants to borrow
1702 Account const borrower{"borrower"};
1703 // Evan will attempt to be naughty
1704 Account const evan{"evan"};
1705 // Do not fund alice
1706 Account const alice{"alice"};
1707
1708 Number const principalRequest = broker.asset(loanAmount).value();
1709 Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0);
1710 BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10);
1711 Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value();
1712 Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value();
1713 Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value();
1714
1715 auto const loanSetFee = Fee(env.current()->fees().base * 2);
1716
1717 auto const pseudoAcct = brokerPseudoAccount(env, broker, lender);
1718
1719 auto const baseFee = env.current()->fees().base;
1720
1721 auto badKeylet = keylet::vault(lender.id(), SeqProxy::rawSequence(env.seq(lender)));
1722 // Try some failure cases
1723 // flags are checked first
1724 env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask),
1725 Sig(sfCounterpartySignature, lender),
1726 loanSetFee,
1728
1729 // field length validation
1730 // sfData: good length, bad account
1731 env(set(evan, broker.brokerID, principalRequest),
1732 Sig(sfCounterpartySignature, borrower),
1734 loanSetFee,
1735 Ter(tefBAD_AUTH));
1736 // sfData: too long
1737 env(set(evan, broker.brokerID, principalRequest),
1738 Sig(sfCounterpartySignature, lender),
1740 loanSetFee,
1741 Ter(temINVALID));
1742
1743 // field range validation
1744 // sfOverpaymentFee: good value, bad account
1745 env(set(evan, broker.brokerID, principalRequest),
1746 Sig(sfCounterpartySignature, borrower),
1747 kOverpaymentFee(kMaxOverpaymentFee),
1748 loanSetFee,
1749 Ter(tefBAD_AUTH));
1750 // sfOverpaymentFee: too big
1751 env(set(evan, broker.brokerID, principalRequest),
1752 Sig(sfCounterpartySignature, lender),
1753 kOverpaymentFee(kMaxOverpaymentFee + 1),
1754 loanSetFee,
1755 Ter(temINVALID));
1756
1757 // sfInterestRate: good value, bad account
1758 env(set(evan, broker.brokerID, principalRequest),
1759 Sig(sfCounterpartySignature, borrower),
1760 kInterestRate(kMaxInterestRate),
1761 loanSetFee,
1762 Ter(tefBAD_AUTH));
1763 env(set(evan, broker.brokerID, principalRequest),
1764 Sig(sfCounterpartySignature, borrower),
1765 kInterestRate(TenthBips32(0)),
1766 loanSetFee,
1767 Ter(tefBAD_AUTH));
1768 // sfInterestRate: too big
1769 env(set(evan, broker.brokerID, principalRequest),
1770 Sig(sfCounterpartySignature, lender),
1771 kInterestRate(kMaxInterestRate + 1),
1772 loanSetFee,
1773 Ter(temINVALID));
1774 // sfInterestRate: too small
1775 env(set(evan, broker.brokerID, principalRequest),
1776 Sig(sfCounterpartySignature, lender),
1777 kInterestRate(TenthBips32(-1)),
1778 loanSetFee,
1779 Ter(temINVALID));
1780
1781 // sfLateInterestRate: good value, bad account
1782 env(set(evan, broker.brokerID, principalRequest),
1783 Sig(sfCounterpartySignature, borrower),
1784 kLateInterestRate(kMaxLateInterestRate),
1785 loanSetFee,
1786 Ter(tefBAD_AUTH));
1787 env(set(evan, broker.brokerID, principalRequest),
1788 Sig(sfCounterpartySignature, borrower),
1789 kLateInterestRate(TenthBips32(0)),
1790 loanSetFee,
1791 Ter(tefBAD_AUTH));
1792 // sfLateInterestRate: too big
1793 env(set(evan, broker.brokerID, principalRequest),
1794 Sig(sfCounterpartySignature, lender),
1795 kLateInterestRate(kMaxLateInterestRate + 1),
1796 loanSetFee,
1797 Ter(temINVALID));
1798 // sfLateInterestRate: too small
1799 env(set(evan, broker.brokerID, principalRequest),
1800 Sig(sfCounterpartySignature, lender),
1801 kLateInterestRate(TenthBips32(-1)),
1802 loanSetFee,
1803 Ter(temINVALID));
1804
1805 // sfCloseInterestRate: good value, bad account
1806 env(set(evan, broker.brokerID, principalRequest),
1807 Sig(sfCounterpartySignature, borrower),
1808 kCloseInterestRate(kMaxCloseInterestRate),
1809 loanSetFee,
1810 Ter(tefBAD_AUTH));
1811 env(set(evan, broker.brokerID, principalRequest),
1812 Sig(sfCounterpartySignature, borrower),
1813 kCloseInterestRate(TenthBips32(0)),
1814 loanSetFee,
1815 Ter(tefBAD_AUTH));
1816 // sfCloseInterestRate: too big
1817 env(set(evan, broker.brokerID, principalRequest),
1818 Sig(sfCounterpartySignature, lender),
1819 kCloseInterestRate(kMaxCloseInterestRate + 1),
1820 loanSetFee,
1821 Ter(temINVALID));
1822 env(set(evan, broker.brokerID, principalRequest),
1823 Sig(sfCounterpartySignature, lender),
1824 kCloseInterestRate(TenthBips32(-1)),
1825 loanSetFee,
1826 Ter(temINVALID));
1827
1828 // sfOverpaymentInterestRate: good value, bad account
1829 env(set(evan, broker.brokerID, principalRequest),
1830 Sig(sfCounterpartySignature, borrower),
1831 kOverpaymentInterestRate(kMaxOverpaymentInterestRate),
1832 loanSetFee,
1833 Ter(tefBAD_AUTH));
1834 env(set(evan, broker.brokerID, principalRequest),
1835 Sig(sfCounterpartySignature, borrower),
1836 kOverpaymentInterestRate(TenthBips32(0)),
1837 loanSetFee,
1838 Ter(tefBAD_AUTH));
1839 // sfOverpaymentInterestRate: too big
1840 env(set(evan, broker.brokerID, principalRequest),
1841 Sig(sfCounterpartySignature, lender),
1842 kOverpaymentInterestRate(kMaxOverpaymentInterestRate + 1),
1843 loanSetFee,
1844 Ter(temINVALID));
1845 env(set(evan, broker.brokerID, principalRequest),
1846 Sig(sfCounterpartySignature, lender),
1847 kOverpaymentInterestRate(TenthBips32(-1)),
1848 loanSetFee,
1849 Ter(temINVALID));
1850
1851 // sfPaymentTotal: good value, bad account
1852 env(set(evan, broker.brokerID, principalRequest),
1853 Sig(sfCounterpartySignature, borrower),
1854 kPaymentTotal(LoanSet::kMinPaymentTotal),
1855 loanSetFee,
1856 Ter(tefBAD_AUTH));
1857 // sfPaymentTotal: too small (there is no max)
1858 env(set(evan, broker.brokerID, principalRequest),
1859 Sig(sfCounterpartySignature, lender),
1860 kPaymentTotal(LoanSet::kMinPaymentTotal - 1),
1861 loanSetFee,
1862 Ter(temINVALID));
1863
1864 // sfPaymentInterval: good value, bad account
1865 env(set(evan, broker.brokerID, principalRequest),
1866 Sig(sfCounterpartySignature, borrower),
1867 kPaymentInterval(LoanSet::kMinPaymentInterval),
1868 loanSetFee,
1869 Ter(tefBAD_AUTH));
1870 // sfPaymentInterval: too small (there is no max)
1871 env(set(evan, broker.brokerID, principalRequest),
1872 Sig(sfCounterpartySignature, lender),
1873 kPaymentInterval(LoanSet::kMinPaymentInterval - 1),
1874 loanSetFee,
1875 Ter(temINVALID));
1876
1877 // sfGracePeriod: good value, bad account
1878 env(set(evan, broker.brokerID, principalRequest),
1879 Sig(sfCounterpartySignature, borrower),
1880 kPaymentInterval(LoanSet::kMinPaymentInterval * 2),
1881 kGracePeriod(LoanSet::kMinPaymentInterval * 2),
1882 loanSetFee,
1883 Ter(tefBAD_AUTH));
1884 // sfGracePeriod: larger than paymentInterval
1885 env(set(evan, broker.brokerID, principalRequest),
1886 Sig(sfCounterpartySignature, lender),
1887 kPaymentInterval(LoanSet::kMinPaymentInterval * 2),
1888 kGracePeriod(LoanSet::kMinPaymentInterval * 3),
1889 loanSetFee,
1890 Ter(temINVALID));
1891
1892 // insufficient fee - single sign
1893 env(set(borrower, broker.brokerID, principalRequest),
1894 Sig(sfCounterpartySignature, lender),
1896 // insufficient fee - multisign
1897 env(signers(lender, 2, {{evan, 1}, {borrower, 1}}));
1898 env(signers(borrower, 2, {{evan, 1}, {lender, 1}}));
1899 env(set(borrower, broker.brokerID, principalRequest),
1900 kCounterparty(lender),
1901 Msig(evan, lender),
1902 Msig(sfCounterpartySignature, evan, borrower),
1903 Fee(env.current()->fees().base * 5 - 1),
1905 // Bad multisign signatures for borrower (Account)
1906 env(set(borrower, broker.brokerID, principalRequest),
1907 kCounterparty(lender),
1908 Msig(alice, issuer),
1909 Msig(sfCounterpartySignature, evan, borrower),
1910 Fee(env.current()->fees().base * 5),
1912 // Bad multisign signatures for issuer (Counterparty)
1913 env(set(borrower, broker.brokerID, principalRequest),
1914 kCounterparty(lender),
1915 Msig(evan, lender),
1916 Msig(sfCounterpartySignature, alice, issuer),
1917 Fee(env.current()->fees().base * 5 - 1),
1919 env(signers(lender, kNone));
1920 env(signers(borrower, kNone));
1921 // multisign sufficient fee, but no signers set up
1922 env(set(borrower, broker.brokerID, principalRequest),
1923 kCounterparty(lender),
1924 Msig(evan, lender),
1925 Msig(sfCounterpartySignature, evan, borrower),
1926 Fee(env.current()->fees().base * 5),
1928 // not the broker owner, no counterparty, not signed by broker
1929 // owner
1930 env(set(borrower, broker.brokerID, principalRequest),
1931 Sig(sfCounterpartySignature, evan),
1932 loanSetFee,
1933 Ter(tefBAD_AUTH));
1934 // not the broker owner, counterparty is borrower
1935 env(set(evan, broker.brokerID, principalRequest),
1936 kCounterparty(borrower),
1937 Sig(sfCounterpartySignature, borrower),
1938 loanSetFee,
1940 // not a LoanBroker object, no counterparty
1941 env(set(lender, badKeylet.key, principalRequest),
1942 Sig(sfCounterpartySignature, evan),
1943 loanSetFee,
1945 // not a LoanBroker object, counterparty is valid
1946 env(set(lender, badKeylet.key, principalRequest),
1947 kCounterparty(borrower),
1948 Sig(sfCounterpartySignature, borrower),
1949 loanSetFee,
1950 Ter(tecNO_ENTRY));
1951 // borrower doesn't exist
1952 env(set(lender, broker.brokerID, principalRequest),
1953 kCounterparty(alice),
1954 Sig(sfCounterpartySignature, alice),
1955 loanSetFee,
1957
1958 // Request more funds than the vault has available
1959 env(set(evan, broker.brokerID, totalVaultRequest + 1),
1960 Sig(sfCounterpartySignature, lender),
1961 loanSetFee,
1963
1964 // Request more funds than the broker's first-loss capital can
1965 // cover.
1966 env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1),
1967 Sig(sfCounterpartySignature, lender),
1968 loanSetFee,
1970
1971 // Frozen trust line / locked MPT issuance
1972 // XRP can not be frozen, but run through the loop anyway to test
1973 // the tecLIMIT_EXCEEDED case
1974 {
1975 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1976 if (!BEAST_EXPECT(brokerSle))
1977 return;
1978
1979 auto const vaultPseudo = [&]() {
1980 auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
1981 if (!BEAST_EXPECT(vaultSle))
1982 {
1983 // This will be wrong, but the test has failed anyway.
1984 return Account{lender};
1985 }
1986 auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount));
1987 return vaultPseudo;
1988 }();
1989
1990 auto const [freeze, deepfreeze, unfreeze, expectedResult] =
1991 [&]() -> std::tuple<
1992 std::function<void(Account const& holder)>,
1993 std::function<void(Account const& holder)>,
1994 std::function<void(Account const& holder)>,
1995 TER> {
1996 // Freeze / lock the asset
1997 std::function<void(Account const& holder)> const empty;
1998 if (broker.asset.native())
1999 {
2000 // XRP can't be frozen
2001 return std::make_tuple(empty, empty, empty, tesSUCCESS);
2002 }
2003 if (broker.asset.holds<Issue>())
2004 {
2005 auto freeze = [&](Account const& holder) {
2006 env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze));
2007 };
2008 auto deepfreeze = [&](Account const& holder) {
2009 env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze));
2010 };
2011 auto unfreeze = [&](Account const& holder) {
2012 env(trust(
2013 issuer, holder[iouCurrency_](0), tfClearFreeze | tfClearDeepFreeze));
2014 };
2015 return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN);
2016 }
2017
2018 auto freeze = [&](Account const& holder) {
2019 mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock});
2020 };
2021 auto unfreeze = [&](Account const& holder) {
2022 mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock});
2023 };
2024 return std::make_tuple(freeze, empty, unfreeze, tecLOCKED);
2025 }();
2026
2027 // Try freezing the accounts that can't be frozen
2028 if (freeze)
2029 {
2030 for (auto const& account : {vaultPseudo, evan})
2031 {
2032 // Freeze the account
2033 freeze(account);
2034
2035 // Try to create a loan with a frozen line
2036 env(set(evan, broker.brokerID, debtMaximumRequest),
2037 Sig(sfCounterpartySignature, lender),
2038 loanSetFee,
2039 Ter(expectedResult));
2040
2041 // Unfreeze the account
2042 BEAST_EXPECT(unfreeze);
2043 unfreeze(account);
2044
2045 // Ensure the line is unfrozen with a request that is fine
2046 // except too it requests more principal than the broker can
2047 // carry
2048 env(set(evan, broker.brokerID, debtMaximumRequest + 1),
2049 Sig(sfCounterpartySignature, lender),
2050 loanSetFee,
2052 }
2053 }
2054
2055 // Deep freeze the borrower, which prevents them from receiving
2056 // funds
2057 if (deepfreeze)
2058 {
2059 // Make sure evan has a trust line that so the issuer can
2060 // freeze it. (Don't need to do this for the borrower,
2061 // because LoanSet will create a line to the borrower
2062 // automatically.)
2063 env(trust(evan, issuer[iouCurrency_](100'000)));
2064
2065 for (auto const& account : {// these accounts can't be frozen, which deep freeze
2066 // implies
2067 vaultPseudo,
2068 evan,
2069 // these accounts can't be deep frozen
2070 lender})
2071 {
2072 // Freeze evan
2073 deepfreeze(account);
2074
2075 // Try to create a loan with a deep frozen line
2076 env(set(evan, broker.brokerID, debtMaximumRequest),
2077 Sig(sfCounterpartySignature, lender),
2078 loanSetFee,
2079 Ter(expectedResult));
2080
2081 // Unfreeze evan
2082 BEAST_EXPECT(unfreeze);
2083 unfreeze(account);
2084
2085 // Ensure the line is unfrozen with a request that is fine
2086 // except too it requests more principal than the broker can
2087 // carry
2088 env(set(evan, broker.brokerID, debtMaximumRequest + 1),
2089 Sig(sfCounterpartySignature, lender),
2090 loanSetFee,
2092 }
2093 }
2094 }
2095
2096 // Finally! Create a loan
2097
2098 auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) {
2099 if (auto const brokerSle = env.le(keylet::loanBroker(brokerID));
2100 BEAST_EXPECT(brokerSle))
2101 {
2102 auto const available = brokerSle->at(sfCoverAvailable);
2103 BEAST_EXPECT(available == expected);
2104 return available;
2105 }
2106 return Number{};
2107 };
2108 auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) {
2109 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
2110 BEAST_EXPECT(brokerSle))
2111 {
2112 BEAST_EXPECT(
2113 state.loanScale >=
2114 (broker.asset.integral()
2115 ? 0
2116 : std::max(
2117 broker.vaultScale(env), state.principalOutstanding.exponent())));
2119 auto const defaultAmount = roundToAsset(
2120 broker.asset,
2121 std::min(
2124 brokerSle->at(sfDebtTotal), broker.params.coverRateMin),
2126 state.totalValue - state.managementFeeOutstanding),
2127 state.loanScale);
2128 return std::make_pair(defaultAmount, brokerSle->at(sfOwner));
2129 }
2130 return std::make_pair(Number{}, AccountID{});
2131 };
2132 auto replenishCover = [&env, &coverAvailable](
2133 BrokerInfo const& broker,
2134 AccountID const& brokerAcct,
2135 Number const& startingCoverAvailable,
2136 Number const& amountToBeCovered) {
2137 coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered);
2139 brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered}));
2140 coverAvailable(broker.brokerID, startingCoverAvailable);
2141 env.close();
2142 };
2143
2144 auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) {
2145 return [&, impair, baseFlag](
2146 Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2147 // toEndOfLife
2148 //
2149 // Default the loan
2150
2151 // Initialize values with the current state
2152 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2153 BEAST_EXPECT(state.flags == baseFlag);
2154
2155 auto const& broker = verifyLoanStatus.broker;
2156 auto const startingCoverAvailable = coverAvailable(
2157 broker.brokerID, broker.asset(broker.params.coverDeposit).number());
2158
2159 if (impair)
2160 {
2161 // Check the vault
2162 bool const canImpair = canImpairLoan(env, broker, state);
2163 // Impair the loan, if possible
2164 env(manage(lender, loanKeylet.key, tfLoanImpair),
2165 canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
2166
2167 if (canImpair)
2168 {
2169 state.flags |= tfLoanImpair;
2170 state.nextPaymentDate = env.now().time_since_epoch().count();
2171
2172 // Once the loan is impaired, it can't be impaired again
2173 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2174 }
2175 verifyLoanStatus(state);
2176 }
2177
2178 auto const nextDueDate = tp{d{state.nextPaymentDate}};
2179
2180 // Can't default the loan yet. The grace period hasn't
2181 // expired
2182 env(manage(lender, loanKeylet.key, tfLoanDefault), 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] = getDefaultInfo(state, broker);
2189
2190 // Default the loan
2191 env(manage(lender, loanKeylet.key, tfLoanDefault));
2192 env.close();
2193
2194 // The LoanBroker just lost some of it's first-loss capital.
2195 // Replenish it.
2196 replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered);
2197
2198 state.flags |= tfLoanDefault;
2199 state.paymentRemaining = 0;
2200 state.totalValue = 0;
2201 state.principalOutstanding = 0;
2202 state.managementFeeOutstanding = 0;
2203 state.nextPaymentDate = 0;
2204 verifyLoanStatus(state);
2205
2206 // Once a loan is defaulted, it can't be managed
2207 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION));
2208 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2209 // Can't make a payment on it either
2210 env(pay(borrower, loanKeylet.key, broker.asset(300)), Ter(tecKILLED));
2211 };
2212 };
2213
2214 auto singlePayment = [&](Keylet const& loanKeylet,
2215 VerifyLoanStatus const& verifyLoanStatus,
2216 LoanState& state,
2217 STAmount const& payoffAmount,
2218 std::uint32_t numPayments,
2219 std::uint32_t baseFlag,
2220 std::uint32_t txFlags) {
2221 // toEndOfLife
2222 //
2223 verifyLoanStatus(state);
2224
2225 // Send some bogus pay transactions
2226 env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags),
2227 Ter(temINVALID));
2228 // broker.asset(80) is less than a single payment, but all these
2229 // checks fail before that matters
2230 env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), Ter(temBAD_AMOUNT));
2231 env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), Ter(tecNO_ENTRY));
2232 env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), Ter(tecNO_PERMISSION));
2233
2234 // TODO: Write a general "isFlag" function? See STObject::isFlag.
2235 // Maybe add a static overloaded member?
2236 if (!(state.flags & lsfLoanOverpayment))
2237 {
2238 // If the loan does not allow overpayments, send a payment that
2239 // tries to make an overpayment. Do not include `txFlags`, so we
2240 // don't end up duplicating the next test transaction.
2241 //
2242 // fixCleanup3_1_3 gates tfLoanOverpayment as a valid flag:
2243 // with fix on → preflight passes, apply returns tecNO_PERMISSION;
2244 // with fix off → preflight rejects the flag, returns temINVALID_FLAG.
2245 bool const hasFix313 = env.current()->rules().enabled(fixCleanup3_1_3);
2246 STAmount const overpayAmount{broker.asset, state.periodicPayment * Number{15, -1}};
2247 XRPAmount const overpayFee{
2248 baseFee * (Number{15, -1} / kLoanPaymentsPerFeeIncrement + 1)};
2249 env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment),
2250 Fee(overpayFee),
2251 Ter(hasFix313 ? TER{tecNO_PERMISSION} : TER{temINVALID_FLAG}));
2252
2253 if (hasFix313)
2254 {
2255 env.disableFeature(fixCleanup3_1_3);
2256 env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment),
2257 Fee(overpayFee),
2259 env.enableFeature(fixCleanup3_1_3);
2260 }
2261 }
2262 // Try to send a payment marked as multiple mutually exclusive
2263 // payment types. Do not include `txFlags`, so we don't duplicate
2264 // the prior test transaction.
2265 env(pay(borrower,
2266 loanKeylet.key,
2267 broker.asset(state.periodicPayment * 2),
2268 tfLoanLatePayment | tfLoanFullPayment),
2270 env(pay(borrower,
2271 loanKeylet.key,
2272 broker.asset(state.periodicPayment * 2),
2273 tfLoanLatePayment | tfLoanOverpayment),
2275 env(pay(borrower,
2276 loanKeylet.key,
2277 broker.asset(state.periodicPayment * 2),
2278 tfLoanOverpayment | tfLoanFullPayment),
2280 env(pay(borrower,
2281 loanKeylet.key,
2282 broker.asset(state.periodicPayment * 2),
2283 tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment),
2285
2286 {
2287 auto const otherAsset =
2288 broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0];
2289 env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), Ter(tecWRONG_ASSET));
2290 }
2291
2292 // Amount doesn't cover a single payment
2293 env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags),
2295
2296 // Get the balance after these failed transactions take
2297 // fees
2298 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
2299
2300 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2301 // Try to pay a little extra to show that it's _not_
2302 // taken
2303 auto const transactionAmount = payoffAmount + broker.asset(10);
2304
2305 // Send a transaction that tries to pay more than the borrowers's
2306 // balance
2307 XRPAmount const badFee{
2308 baseFee *
2309 (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment /
2310 kLoanPaymentsPerFeeIncrement +
2311 1)};
2312 env(pay(borrower,
2313 loanKeylet.key,
2314 STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2},
2315 txFlags),
2316 Fee(badFee),
2318
2319 XRPAmount const goodFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)};
2320 env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), Fee(goodFee));
2321
2322 env.close();
2323
2324 // log << env.meta()->getJson() << std::endl;
2325
2326 // Need to account for fees if the loan is in XRP
2327 PrettyAmount adjustment = broker.asset(0);
2328 if (broker.asset.native())
2329 {
2330 adjustment = badFee + goodFee;
2331 }
2332
2333 state.paymentRemaining = 0;
2334 state.principalOutstanding = 0;
2335 state.totalValue = 0;
2336 state.managementFeeOutstanding = 0;
2337 state.previousPaymentDate =
2338 state.nextPaymentDate + (state.paymentInterval * (numPayments - 1));
2339 state.nextPaymentDate = 0;
2340 verifyLoanStatus(state);
2341
2342 verifyLoanStatus.checkPayment(
2343 state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment);
2344
2345 // Can't impair or default a paid off loan
2346 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2347 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION));
2348 };
2349
2350 auto fullPayment = [&](std::uint32_t baseFlag) {
2351 return [&, baseFlag](
2352 Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2353 // toEndOfLife
2354 //
2355 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2356 env.close(state.startDate + 20s);
2357 auto const loanAge = (env.now() - state.startDate).count();
2358 BEAST_EXPECT(loanAge == 30);
2359
2360 // Full payoff amount will consist of
2361 // 1. principal outstanding (1000)
2362 // 2. accrued interest (at 12%)
2363 // 3. prepayment penalty (closeInterest at 3.6%)
2364 // 4. close payment fee (4)
2365 // Calculate these values without the helper functions
2366 // to verify they're working correctly The numbers in
2367 // the below BEAST_EXPECTs may not hold across assets.
2368 Number const interval = state.paymentInterval;
2369 auto const periodicRate = interval * Number(12, -2) / kSecondsInYear;
2370 BEAST_EXPECT(
2371 periodicRate == Number(2283105022831050228ULL, -24, Number::Normalized{}));
2372 STAmount const principalOutstanding{broker.asset, state.principalOutstanding};
2373 STAmount const accruedInterest{
2374 broker.asset, state.principalOutstanding * periodicRate * loanAge / interval};
2375 BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19)));
2376 STAmount const prepaymentPenalty{
2377 broker.asset, state.principalOutstanding * Number(36, -3)};
2378 BEAST_EXPECT(prepaymentPenalty == broker.asset(36));
2379 STAmount const closePaymentFee = broker.asset(4);
2380 auto const payoffAmount = roundToScale(
2381 principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee,
2382 state.loanScale);
2383 BEAST_EXPECT(
2384 payoffAmount ==
2386 broker.asset,
2387 broker.asset(Number(1040000114155251, -12)).number(),
2388 state.loanScale));
2389
2390 // The terms of this loan actually make the early payoff
2391 // more expensive than just making payments
2392 BEAST_EXPECT(
2393 payoffAmount >
2394 state.paymentRemaining * (state.periodicPayment + broker.asset(2).value()));
2395
2396 singlePayment(
2397 loanKeylet,
2398 verifyLoanStatus,
2399 state,
2400 payoffAmount,
2401 1,
2402 baseFlag,
2403 tfLoanFullPayment);
2404 };
2405 };
2406
2407 auto combineAllPayments = [&](std::uint32_t baseFlag) {
2408 return
2409 [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2410 // toEndOfLife
2411 //
2412
2413 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2414 env.close();
2415
2416 BEAST_EXPECT(
2417 STAmount(broker.asset, state.periodicPayment) ==
2418 broker.asset(Number(8333457002039338267, -17)));
2419
2420 // Make all the payments in one transaction
2421 // service fee is 2
2422 auto const startingPayments = state.paymentRemaining;
2423 STAmount const payoffAmount = [&]() {
2425 auto const rawPayoff =
2426 startingPayments * (state.periodicPayment + broker.asset(2).value());
2427 STAmount payoffAmount{broker.asset, rawPayoff};
2428 BEAST_EXPECTS(
2429 payoffAmount == broker.asset(Number(1024014840244721, -12)),
2430 to_string(payoffAmount));
2431 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2432
2433 payoffAmount = roundToScale(payoffAmount, state.loanScale);
2434
2435 return payoffAmount;
2436 }();
2437
2438 auto const totalPayoffValue =
2439 state.totalValue + startingPayments * broker.asset(2).value();
2440 STAmount const totalPayoffAmount{broker.asset, totalPayoffValue};
2441
2442 BEAST_EXPECTS(
2443 totalPayoffAmount == payoffAmount,
2444 "Payoff amount: " + to_string(payoffAmount) +
2445 ". Total Value: " + to_string(totalPayoffAmount));
2446
2447 singlePayment(
2448 loanKeylet,
2449 verifyLoanStatus,
2450 state,
2451 payoffAmount,
2452 state.paymentRemaining,
2453 baseFlag,
2454 0);
2455 };
2456 };
2457
2458 // There are a lot of fields that can be set on a loan, but most
2459 // of them only affect the "math" when a payment is made. The
2460 // only one that really affects behavior is the
2461 // `tfLoanOverpayment` flag.
2462 lifecycle(
2463 caseLabel,
2464 "Loan overpayment allowed - Impair and Default",
2465 env,
2466 loanAmount,
2467 interestExponent,
2468 lender,
2469 borrower,
2470 evan,
2471 broker,
2472 pseudoAcct,
2473 tfLoanOverpayment,
2474 defaultImmediately(lsfLoanOverpayment));
2475
2476 lifecycle(
2477 caseLabel,
2478 "Loan overpayment prohibited - Impair and Default",
2479 env,
2480 loanAmount,
2481 interestExponent,
2482 lender,
2483 borrower,
2484 evan,
2485 broker,
2486 pseudoAcct,
2487 0,
2488 defaultImmediately(0));
2489
2490 lifecycle(
2491 caseLabel,
2492 "Loan overpayment allowed - Default without Impair",
2493 env,
2494 loanAmount,
2495 interestExponent,
2496 lender,
2497 borrower,
2498 evan,
2499 broker,
2500 pseudoAcct,
2501 tfLoanOverpayment,
2502 defaultImmediately(lsfLoanOverpayment, false));
2503
2504 lifecycle(
2505 caseLabel,
2506 "Loan overpayment prohibited - Default without Impair",
2507 env,
2508 loanAmount,
2509 interestExponent,
2510 lender,
2511 borrower,
2512 evan,
2513 broker,
2514 pseudoAcct,
2515 0,
2516 defaultImmediately(0, false));
2517
2518 lifecycle(
2519 caseLabel,
2520 "Loan overpayment prohibited - Pay off immediately",
2521 env,
2522 loanAmount,
2523 interestExponent,
2524 lender,
2525 borrower,
2526 evan,
2527 broker,
2528 pseudoAcct,
2529 0,
2530 fullPayment(0));
2531
2532 lifecycle(
2533 caseLabel,
2534 "Loan overpayment allowed - Pay off immediately",
2535 env,
2536 loanAmount,
2537 interestExponent,
2538 lender,
2539 borrower,
2540 evan,
2541 broker,
2542 pseudoAcct,
2543 tfLoanOverpayment,
2544 fullPayment(lsfLoanOverpayment));
2545
2546 lifecycle(
2547 caseLabel,
2548 "Loan overpayment prohibited - Combine all payments",
2549 env,
2550 loanAmount,
2551 interestExponent,
2552 lender,
2553 borrower,
2554 evan,
2555 broker,
2556 pseudoAcct,
2557 0,
2558 combineAllPayments(0));
2559
2560 lifecycle(
2561 caseLabel,
2562 "Loan overpayment allowed - Combine all payments",
2563 env,
2564 loanAmount,
2565 interestExponent,
2566 lender,
2567 borrower,
2568 evan,
2569 broker,
2570 pseudoAcct,
2571 tfLoanOverpayment,
2572 combineAllPayments(lsfLoanOverpayment));
2573
2574 lifecycle(
2575 caseLabel,
2576 "Loan overpayment prohibited - Make payments",
2577 env,
2578 loanAmount,
2579 interestExponent,
2580 lender,
2581 borrower,
2582 evan,
2583 broker,
2584 pseudoAcct,
2585 0,
2586 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2587 // toEndOfLife
2588 //
2589 // Draw and make multiple payments
2590 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2591 BEAST_EXPECT(state.flags == 0);
2592 env.close();
2593
2594 verifyLoanStatus(state);
2595
2596 env.close(state.startDate + 20s);
2597 auto const loanAge = (env.now() - state.startDate).count();
2598 BEAST_EXPECT(loanAge == 30);
2599
2600 // Periodic payment amount will consist of
2601 // 1. principal outstanding (1000)
2602 // 2. interest interest rate (at 12%)
2603 // 3. payment interval (600s)
2604 // 4. loan service fee (2)
2605 // Calculate these values without the helper functions
2606 // to verify they're working correctly The numbers in
2607 // the below BEAST_EXPECTs may not hold across assets.
2608 Number const interval = state.paymentInterval;
2609 auto const periodicRate = interval * Number(12, -2) / kSecondsInYear;
2610 BEAST_EXPECT(
2611 periodicRate == Number(2283105022831050228, -24, Number::Normalized{}));
2612 STAmount const roundedPeriodicPayment{
2613 broker.asset,
2614 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
2615
2616 testcase << currencyLabel << " Payment components: "
2617 << "Payments remaining, rawInterest, rawPrincipal, "
2618 "rawMFee, trackedValueDelta, trackedPrincipalDelta, "
2619 "trackedInterestDelta, trackedMgmtFeeDelta, special";
2620
2621 auto const serviceFee = broker.asset(2);
2622
2623 BEAST_EXPECT(
2624 roundedPeriodicPayment ==
2626 broker.asset(
2627 Number(8333457002039338267, -17), Number::RoundingMode::Upward),
2628 state.loanScale,
2630 // 83334570.01162141
2631 // Include the service fee
2632 STAmount const totalDue = roundToScale(
2633 roundedPeriodicPayment + serviceFee,
2634 state.loanScale,
2636 // Only check the first payment since the rounding
2637 // may drift as payments are made
2638 BEAST_EXPECT(
2639 totalDue ==
2641 broker.asset(
2642 Number(8533457002039338267, -17), Number::RoundingMode::Upward),
2643 state.loanScale,
2645
2646 {
2647 auto const raw = computeTheoreticalLoanState(
2648 env.current()->rules(),
2649 state.periodicPayment,
2650 periodicRate,
2651 state.paymentRemaining,
2652 broker.params.managementFeeRate);
2653 auto const rounded = constructLoanState(
2654 state.totalValue,
2655 state.principalOutstanding,
2656 state.managementFeeOutstanding);
2657 testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining
2658 << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", "
2659 << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", "
2660 << rounded.principalOutstanding << ", " << rounded.interestDue << ", "
2661 << rounded.managementFeeDue;
2662 }
2663
2664 // Try to pay a little extra to show that it's _not_
2665 // taken
2666 STAmount const transactionAmount =
2667 STAmount{broker.asset, totalDue} + broker.asset(10);
2668 // Only check the first payment since the rounding
2669 // may drift as payments are made
2670 BEAST_EXPECT(
2671 transactionAmount ==
2673 broker.asset(Number(9533457002039400, -14), Number::RoundingMode::Upward),
2674 state.loanScale,
2676
2677 auto const initialState = state;
2679 .trackedValueDelta = 0,
2680 .trackedPrincipalDelta = 0,
2681 .trackedManagementFeeDelta = 0};
2682 Number totalInterestPaid = 0;
2683 std::size_t totalPaymentsMade = 0;
2684
2686 env.current()->rules(),
2687 state.periodicPayment,
2688 periodicRate,
2689 state.paymentRemaining,
2690 broker.params.managementFeeRate);
2691
2692 while (state.paymentRemaining > 0)
2693 {
2694 // Compute the expected principal amount
2695 auto const paymentComponents = xrpl::detail::computePaymentComponents(
2696 env.current()->rules(),
2697 broker.asset.raw(),
2698 state.loanScale,
2699 state.totalValue,
2700 state.principalOutstanding,
2701 state.managementFeeOutstanding,
2702 state.periodicPayment,
2703 periodicRate,
2704 state.paymentRemaining,
2705 broker.params.managementFeeRate);
2706
2707 BEAST_EXPECTS(
2708 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2709 paymentComponents.trackedValueDelta <= roundedPeriodicPayment,
2710 "Delta: " + to_string(paymentComponents.trackedValueDelta) +
2711 ", periodic payment: " + to_string(roundedPeriodicPayment));
2712
2713 xrpl::LoanState const nextTrueState = computeTheoreticalLoanState(
2714 env.current()->rules(),
2715 state.periodicPayment,
2716 periodicRate,
2717 state.paymentRemaining - 1,
2718 broker.params.managementFeeRate);
2719 xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState;
2720
2721 testcase << currencyLabel << " Payment components: " << state.paymentRemaining
2722 << ", " << deltas.interest << ", " << deltas.principal << ", "
2723 << deltas.managementFee << ", " << paymentComponents.trackedValueDelta
2724 << ", " << paymentComponents.trackedPrincipalDelta << ", "
2725 << paymentComponents.trackedInterestPart() << ", "
2726 << paymentComponents.trackedManagementFeeDelta << ", "
2727 << [&]() -> char const* {
2728 if (paymentComponents.specialCase ==
2730 return "final";
2731 if (paymentComponents.specialCase ==
2733 return "extra";
2734 return "none";
2735 }();
2736
2737 auto const totalDueAmount = STAmount{
2738 broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()};
2739
2740 // Due to the rounding algorithms to keep the interest and
2741 // principal in sync with "true" values, the computed amount
2742 // may be a little less than the rounded fixed payment
2743 // amount. For integral types, the difference should be < 3
2744 // (1 unit for each of the interest and management fee). For
2745 // IOUs, the difference should be after the 8th digit.
2746 Number const diff = totalDue - totalDueAmount;
2747 BEAST_EXPECT(
2748 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2749 diff == beast::kZero ||
2750 (diff > beast::kZero &&
2751 ((broker.asset.integral() && (static_cast<Number>(diff) < 3)) ||
2752 (state.loanScale - diff.exponent() > 13))));
2753
2754 BEAST_EXPECT(
2755 paymentComponents.trackedValueDelta ==
2756 paymentComponents.trackedPrincipalDelta +
2757 paymentComponents.trackedInterestPart() +
2758 paymentComponents.trackedManagementFeeDelta);
2759 BEAST_EXPECT(
2760 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2761 paymentComponents.trackedValueDelta <= roundedPeriodicPayment);
2762
2763 BEAST_EXPECT(
2764 state.paymentRemaining < 12 ||
2766 broker.asset,
2767 deltas.principal,
2768 state.loanScale,
2771 broker.asset(
2772 Number(8333228691531218890, -17), Number::RoundingMode::Upward),
2773 state.loanScale,
2775 BEAST_EXPECT(
2776 paymentComponents.trackedPrincipalDelta >= beast::kZero &&
2777 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
2778 BEAST_EXPECT(
2779 paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final ||
2780 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
2781 BEAST_EXPECT(
2782 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2783 (state.periodicPayment.exponent() -
2784 (deltas.principal + deltas.interest + deltas.managementFee -
2785 state.periodicPayment)
2786 .exponent()) > 14);
2787
2788 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
2789
2790 if (canImpairLoan(env, broker, state))
2791 {
2792 // Making a payment will unimpair the loan
2793 env(manage(lender, loanKeylet.key, tfLoanImpair));
2794 }
2795
2796 env.close();
2797
2798 // Make the payment
2799 env(pay(borrower, loanKeylet.key, transactionAmount));
2800
2801 env.close();
2802
2803 // Need to account for fees if the loan is in XRP
2804 PrettyAmount adjustment = broker.asset(0);
2805 if (broker.asset.native())
2806 {
2807 adjustment = env.current()->fees().base;
2808 }
2809
2810 // Check the result
2811 verifyLoanStatus.checkPayment(
2812 state.loanScale,
2813 borrower,
2814 borrowerBalanceBeforePayment,
2815 totalDueAmount,
2816 adjustment);
2817
2818 --state.paymentRemaining;
2819 state.previousPaymentDate = state.nextPaymentDate;
2820 if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final)
2821 {
2822 state.paymentRemaining = 0;
2823 state.nextPaymentDate = 0;
2824 }
2825 else
2826 {
2827 state.nextPaymentDate += state.paymentInterval;
2828 }
2829 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
2830 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
2831 state.totalValue -= paymentComponents.trackedValueDelta;
2832
2833 verifyLoanStatus(state);
2834
2835 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
2836 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
2837 totalPaid.trackedManagementFeeDelta +=
2838 paymentComponents.trackedManagementFeeDelta;
2839 totalInterestPaid += paymentComponents.trackedInterestPart();
2840 ++totalPaymentsMade;
2841
2842 currentTrueState = nextTrueState;
2843 }
2844
2845 // Loan is paid off
2846 BEAST_EXPECT(state.paymentRemaining == 0);
2847 BEAST_EXPECT(state.principalOutstanding == 0);
2848
2849 // Make sure all the payments add up
2850 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
2851 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
2852 BEAST_EXPECT(
2853 totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
2854 // This is almost a tautology given the previous checks, but
2855 // check it anyway for completeness.
2856 BEAST_EXPECT(
2857 totalInterestPaid ==
2858 initialState.totalValue -
2859 (initialState.principalOutstanding +
2860 initialState.managementFeeOutstanding));
2861 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
2862
2863 // Can't impair or default a paid off loan
2864 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2865 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION));
2866 });
2867
2868#if LOAN_TODO
2869 // TODO
2870
2871 /*
2872 LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also
2873 borrower) tries to do the payment. Here's the scenario: Create a XRP
2874 loan with loan broker as borrower, loan origination fee and loan service
2875 fee. Loan broker makes the first payment with periodic payment and loan
2876 service fee.
2877 */
2878
2879 auto time = [&](std::string label, std::function<void()> timed) {
2880 if (!BEAST_EXPECT(timed))
2881 return;
2882
2883 using clock_type = std::chrono::steady_clock;
2884 using duration_type = std::chrono::milliseconds;
2885
2886 auto const start = clock_type::now();
2887 timed();
2888 auto const duration =
2889 std::chrono::duration_cast<duration_type>(clock_type::now() - start);
2890
2891 log << label << " took " << duration.count() << "ms" << std::endl;
2892
2893 return duration;
2894 };
2895
2896 lifecycle(
2897 caseLabel,
2898 "timing",
2899 env,
2900 loanAmount,
2901 interestExponent,
2902 lender,
2903 borrower,
2904 evan,
2905 broker,
2906 pseudoAcct,
2907 tfLoanOverpayment,
2908 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2909 // Estimate optimal values for kLoanPaymentsPerFeeIncrement and
2910 // kLoanMaximumPaymentsPerTransaction.
2911 using namespace loan;
2912
2913 auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet);
2914 auto const serviceFee = broker.asset(2).value();
2915
2916 STAmount const totalDue{
2917 broker.asset,
2919 broker.asset, state.periodicPayment + serviceFee, state.loanScale)};
2920
2921 // Make a single payment
2922 time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); });
2923 env.close();
2924
2925 // Make all but the final payment
2926 auto const numPayments = (state.paymentRemaining - 2);
2927 STAmount const bigPayment{broker.asset, totalDue * numPayments};
2928 XRPAmount const bigFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)};
2929 time("ten payments", [&]() {
2930 env(pay(borrower, loanKeylet.key, bigPayment), Fee(bigFee));
2931 });
2932 env.close();
2933
2934 time("final payment", [&]() {
2935 // Make the final payment
2936 env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1}));
2937 });
2938 env.close();
2939 });
2940
2941 lifecycle(
2942 caseLabel,
2943 "Loan overpayment allowed - Explicit overpayment",
2944 env,
2945 loanAmount,
2946 interestExponent,
2947 lender,
2948 borrower,
2949 evan,
2950 broker,
2951 pseudoAcct,
2952 tfLoanOverpayment,
2953 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2954
2955 lifecycle(
2956 caseLabel,
2957 "Loan overpayment prohibited - Late payment",
2958 env,
2959 loanAmount,
2960 interestExponent,
2961 lender,
2962 borrower,
2963 evan,
2964 broker,
2965 pseudoAcct,
2966 tfLoanOverpayment,
2967 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2968
2969 lifecycle(
2970 caseLabel,
2971 "Loan overpayment allowed - Late payment",
2972 env,
2973 loanAmount,
2974 interestExponent,
2975 lender,
2976 borrower,
2977 evan,
2978 broker,
2979 pseudoAcct,
2980 tfLoanOverpayment,
2981 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2982
2983 lifecycle(
2984 caseLabel,
2985 "Loan overpayment allowed - Late payment and overpayment",
2986 env,
2987 loanAmount,
2988 interestExponent,
2989 lender,
2990 borrower,
2991 evan,
2992 broker,
2993 pseudoAcct,
2994 tfLoanOverpayment,
2995 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2996
2997#endif
2998 }
2999};
3000
3001} // namespace xrpl::test
A testsuite class.
Definition suite.h:52
LogOs< char > log
Logging output stream.
Definition suite.h:150
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:155
constexpr bool native() const
Definition Asset.h:125
constexpr bool holds() const
Definition Asset.h:177
A currency issued by an account.
Definition Issue.h:18
static constexpr std::uint32_t kDefaultPaymentTotal
static constexpr std::uint32_t kMinPaymentTotal
static constexpr std::uint32_t kDefaultPaymentInterval
static constexpr std::uint32_t kMinPaymentInterval
std::chrono::time_point< NetClock > time_point
Definition chrono.h:48
std::chrono::duration< rep, period > duration
Definition chrono.h:47
Number is a floating point type that can represent a wide range of values.
Definition Number.h:351
Number truncate() const noexcept
constexpr int exponent() const noexcept
Returns the exponent of the external view of the Number.
Definition Number.h:714
STAmount const & value() const noexcept
Definition STAmount.h:610
static constexpr SeqProxy rawSequence(std::uint32_t v)
Factory function to return a sequence-based SeqProxy.
Definition SeqProxy.h:62
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.
jtx::PrettyAsset createFundedIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=100 '000 '000, Number const &borrowerPay=1 '000 '000)
FeatureBitset const all_
void runLoan(AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, FeatureBitset features)
jtx::Account brokerPseudoAccount(jtx::Env const &env, BrokerInfo const &broker, jtx::Account const &fallback)
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const &params=BrokerParameters::defaults())
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)
LoanState getCurrentState(jtx::Env const &env, BrokerInfo const &broker, Keylet const &loanKeylet)
Get the state without checking anything.
Keylet nextLoanKeylet(jtx::Env const &env, BrokerInfo const &broker)
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 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())
std::optional< std::tuple< BrokerInfo, Keylet, jtx::Account > > createLoan(jtx::Env &env, AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
static jtx::PrettyAsset createFundedRippleIouAsset(jtx::Env &env, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, Number const &lenderPay=1 '000 '000, Number const &borrowerPay=1 '000 '000)
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::string const iouCurrency_
bool canImpairLoan(jtx::Env const &env, BrokerInfo const &broker, LoanState const &state)
static std::string getCurrencyLabel(Asset const &asset)
static void topUpBorrower(jtx::Env &env, BrokerInfo const &broker, jtx::Account const &issuer, jtx::Account const &borrower, LoanState const &state, std::optional< Number > const &servFee)
Immutable cryptographic account descriptor.
Definition jtx/Account.h:21
AccountID id() const
Returns the Account ID.
A transaction testing environment.
Definition Env.h:161
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:750
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:311
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
Definition Env.cpp:266
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
Definition Env.cpp:323
void enableFeature(uint256 const feature)
Definition Env.cpp:709
void disableFeature(uint256 const feature)
Definition Env.cpp:717
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:302
Account const & master
Definition Env.h:165
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:721
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
beast::Journal const journal
Definition Env.h:204
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:377
NetClock::time_point now()
Returns the current network time.
Definition Env.h:326
Set the fee on a JTx.
Definition fee.h:20
Converts to IOU Issue or STAmount.
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:447
void set(MPTSet const &set={})
Definition mpt.cpp:467
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:241
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:353
MPTID const & issuanceID() const
Definition mpt.h:641
Converts to MPT Issue or STAmount.
Set a multisignature on a JTx.
Definition multisign.h:53
Set the regular signature on a JTx.
Definition sig.h:19
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:18
T duration_cast(T... args)
T endl(T... args)
T make_pair(T... args)
T make_tuple(T... args)
T max(T... args)
T min(T... args)
constexpr Zero kZero
Definition Zero.h:30
STL namespace.
PaymentComponents computePaymentComponents(Rules const &rules, 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 computation functions.
Definition Indexes.h:40
Keylet loan(uint256 const &loanBrokerID, SeqProxy const &loanSeq) noexcept
Definition Indexes.cpp:573
Keylet vault(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:561
Keylet loanBroker(AccountID const &owner, SeqProxy const &seq) noexcept
Definition Indexes.cpp:567
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:198
Deposit preauthorize operations.
Definition deposit.h:16
json::Value coverDeposit(AccountID const &account, uint256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value del(AccountID const &account, uint256 const &brokerID, uint32_t flags)
json::Value set(AccountID const &account, uint256 const &loanBrokerID, Number principalRequested, std::uint32_t flags)
static NoneT const kNone
Definition tags.h:9
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
std::uint32_t ownerCount(Env const &env, Account const &account)
static Number number(STAmount const &a)
Definition AMM.cpp:44
FeatureBitset testableAmendments()
Definition Env.h:92
auto const kData
General field definitions, or fields used in multiple transaction namespaces.
std::array< Account, 1+sizeof...(Args)> noripple(Account const &account, Args const &... args)
Designate accounts as no-ripple in Env::fund.
Definition Env.h:86
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
json::Value signers(Account const &account, std::uint32_t quorum, std::vector< Signer > const &v)
Definition multisign.cpp:31
json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
Definition flags.cpp:15
static MPTInit const kMptInitNoFund
Definition mpt.h:172
static STAmount accountReserve(jtx::Env &env, std::uint32_t count=1)
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:54
static constexpr Number kNumZero
Definition Number.h:663
@ telINSUF_FEE_P
Definition TER.h:43
@ terNO_ACCOUNT
Definition TER.h:213
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:108
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:126
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:138
int getAssetsTotalScale(SLE::const_ref vaultSle)
@ tefBAD_SIGNATURE
Definition TER.h:171
@ tefBAD_AUTH
Definition TER.h:161
@ tefNOT_MULTI_SIGNING
Definition TER.h:173
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:454
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:453
VaultKind
Vault kind.
Definition Protocol.h:335
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:651
constexpr std::size_t kMaxDataPayloadLength
The maximum length of Data payload.
Definition Protocol.h:302
TER checkLoanGuards(Asset const &vaultAsset, Number const &principalRequested, bool expectInterest, std::uint32_t paymentTotal, LoanProperties const &properties, beast::Journal j)
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::RoundingMode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
constexpr TenthBips32 kTenthBipsPerUnity(kBipsPerUnity.value() *10)
LedgerSpecificFlags
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:735
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:34
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ temINVALID
Definition TER.h:98
@ temINVALID_FLAG
Definition TER.h:99
@ temBAD_AMOUNT
Definition TER.h:77
@ temBAD_SIGNER
Definition TER.h:103
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
Definition TER.h:647
@ tecWRONG_ASSET
Definition TER.h:363
@ tecLOCKED
Definition TER.h:361
@ tecNO_ENTRY
Definition TER.h:309
@ tecTOO_SOON
Definition TER.h:321
@ tecFROZEN
Definition TER.h:306
@ tecINSUFFICIENT_FUNDS
Definition TER.h:328
@ tecKILLED
Definition TER.h:319
@ tecLIMIT_EXCEEDED
Definition TER.h:364
@ tecNO_PERMISSION
Definition TER.h:308
@ tecHAS_OBLIGATIONS
Definition TER.h:320
@ tecINSUFFICIENT_PAYMENT
Definition TER.h:330
static constexpr std::uint32_t kSecondsInYear
VaultVersion getVaultVersion(SLE::const_ref vault)
Resolves a Vault's LEVersion, the single point every accounting touch point should call to determine ...
LoanProperties computeLoanProperties(Rules const &rules, Asset const &asset, Number const &principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
BaseUInt< 256 > uint256
Definition base_uint.h:580
@ tesSUCCESS
Definition TER.h:245
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
Number total() const
Calculates the total change across all components.
std::optional< std::uint32_t > subscriptionDate
std::optional< std::uint32_t > redemptionDate
int vaultScale(jtx::Env const &env) const
BrokerInfo(jtx::PrettyAsset const &asset, Keylet const &brokerKeylet, Keylet const &vaultKeylet, BrokerParameters p, std::optional< std::uint32_t > subscriptionDate=std::nullopt, std::optional< std::uint32_t > redemptionDate=std::nullopt)
static BrokerParameters const & defaults()
Number maxCoveredLoanValue(Number const &currentDebt) const
std::optional< std::uint8_t > vaultScale
std::optional< std::uint32_t > payTotal
std::optional< std::uint32_t > flags
std::optional< std::uint32_t > gracePd
jtx::JTx operator()(jtx::Env &env, BrokerInfo const &broker, FN const &... fN) const
std::optional< TenthBips32 > overpaymentInterest
std::optional< TenthBips32 > overFee
std::optional< TenthBips32 > lateInterest
std::optional< TenthBips32 > closeInterest
std::optional< TenthBips32 > interest
std::optional< std::uint32_t > payInterval
static PaymentParameters const & defaults()
Helper class to compare the expected state of a loan and loan broker against the data in 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 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 checkPayment(std::int32_t loanScale, jtx::Account const &account, jtx::PrettyAmount const &balanceBefore, STAmount const &expectedPayment, jtx::PrettyAmount const &adjustment) const
void operator()(LoanState const &state) const
Checks both the loan and broker expect states against the ledger.
VerifyLoanStatus(jtx::Env const &env, BrokerInfo const &broker, jtx::Account const &pseudo, Keylet const &keylet)
Execution context for applying a JSON transaction.
Definition JTx.h:27
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
STAmount const & value() const
T time_since_epoch(T... args)
T value_or(T... args)