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