xrpld
Loading...
Searching...
No Matches
Loan_test.cpp
1#include <xrpl/beast/unit_test/suite.h>
2//
3#include <test/jtx/Account.h>
4#include <test/jtx/Env.h>
5#include <test/jtx/TestHelpers.h>
6#include <test/jtx/amount.h>
7#include <test/jtx/batch.h>
8#include <test/jtx/credentials.h>
9#include <test/jtx/envconfig.h>
10#include <test/jtx/fee.h>
11#include <test/jtx/flags.h>
12#include <test/jtx/jtx_json.h>
13#include <test/jtx/mpt.h>
14#include <test/jtx/multisign.h>
15#include <test/jtx/noop.h>
16#include <test/jtx/offer.h>
17#include <test/jtx/pay.h>
18#include <test/jtx/permissioned_domains.h>
19#include <test/jtx/seq.h>
20#include <test/jtx/sig.h>
21#include <test/jtx/tags.h>
22#include <test/jtx/ter.h>
23#include <test/jtx/trust.h>
24#include <test/jtx/txflags.h>
25#include <test/jtx/utility.h>
26#include <test/jtx/vault.h>
27
28#include <xrpld/rpc/detail/Handler.h>
29
30#include <xrpl/basics/Number.h>
31#include <xrpl/basics/base_uint.h>
32#include <xrpl/basics/chrono.h>
33#include <xrpl/basics/strHex.h>
34#include <xrpl/beast/utility/Journal.h>
35#include <xrpl/beast/utility/Zero.h>
36#include <xrpl/beast/xor_shift_engine.h>
37#include <xrpl/json/json_value.h>
38#include <xrpl/json/to_string.h>
39#include <xrpl/ledger/helpers/LendingHelpers.h>
40#include <xrpl/protocol/Asset.h>
41#include <xrpl/protocol/Feature.h>
42#include <xrpl/protocol/HashPrefix.h>
43#include <xrpl/protocol/Indexes.h>
44#include <xrpl/protocol/Issue.h>
45#include <xrpl/protocol/KeyType.h>
46#include <xrpl/protocol/LedgerFormats.h>
47#include <xrpl/protocol/MPTIssue.h>
48#include <xrpl/protocol/Protocol.h>
49#include <xrpl/protocol/SField.h>
50#include <xrpl/protocol/STAmount.h>
51#include <xrpl/protocol/STTx.h>
52#include <xrpl/protocol/SecretKey.h>
53#include <xrpl/protocol/Serializer.h>
54#include <xrpl/protocol/TER.h>
55#include <xrpl/protocol/TxFlags.h>
56#include <xrpl/protocol/TxFormats.h>
57#include <xrpl/protocol/Units.h>
58#include <xrpl/protocol/XRPAmount.h>
59#include <xrpl/protocol/jss.h>
60#include <xrpl/server/LoadFeeTrack.h>
61#include <xrpl/tx/transactors/lending/LoanSet.h>
62#include <xrpl/tx/transactors/system/Batch.h>
63
64#include <algorithm>
65#include <array>
66#include <chrono>
67#include <cstddef>
68#include <cstdint>
69#include <cstdlib>
70#include <functional>
71#include <initializer_list>
72#include <limits>
73#include <map>
74#include <optional>
75#include <ostream>
76#include <random>
77#include <sstream>
78#include <stdexcept>
79#include <string>
80#include <string_view>
81#include <tuple>
82#include <type_traits>
83#include <utility>
84#include <vector>
85
86namespace xrpl::test {
87
89{
90protected:
91 // Ensure that all the features needed for Lending Protocol are included,
92 // even if they are set to unsupported.
93
96
97 void
99 {
100 testcase("Disabled");
101 // Lending Protocol depends on Single Asset Vault (SAV). Test
102 // combinations of the two amendments.
103 // Single Asset Vault depends on MPTokensV1, but don't test every combo
104 // of that.
105 using namespace jtx;
106 auto failAll = [this](FeatureBitset features) {
107 Env env(*this, features);
108
109 Account const alice{"alice"};
110 Account const bob{"bob"};
111 env.fund(XRP(10000), alice, bob);
112
113 auto const keylet = keylet::loanBroker(alice, env.seq(alice));
114
115 using namespace std::chrono_literals;
116 using namespace loan;
117
118 // counter party signature is optional on LoanSet. Confirm that by
119 // sending transaction without one.
120 auto setTx = env.jt(set(alice, keylet.key, Number(10000)), Ter(temDISABLED));
121 env(setTx);
122
123 // All loan transactions are disabled.
124 // 1. LoanSet
125 setTx = env.jt(setTx, Sig(sfCounterpartySignature, bob), Ter(temDISABLED));
126 env(setTx);
127 // Actual sequence will be based off the loan broker, but we
128 // obviously don't have one of those if the amendment is disabled
129 auto const loanKeylet = keylet::loan(keylet.key, env.seq(alice));
130 // Other Loan transactions are disabled, too.
131 // 2. LoanDelete
132 env(del(alice, loanKeylet.key), Ter(temDISABLED));
133 // 3. LoanManage
134 env(manage(alice, loanKeylet.key, tfLoanImpair), Ter(temDISABLED));
135 // 4. LoanPay
136 env(pay(alice, loanKeylet.key, XRP(500)), Ter(temDISABLED));
137 };
138 failAll(all_ - featureMPTokensV1);
139 failAll(all_ - featureSingleAssetVault - featureLendingProtocol);
140 failAll(all_ - featureSingleAssetVault);
141 failAll(all_ - featureLendingProtocol);
142 }
143
145 {
146 Number vaultDeposit = 1'000'000;
147 Number debtMax = 25'000;
149 int coverDeposit = 1000;
152 std::string data = {}; // NOLINT(readability-redundant-member-init)
154 // If set, the vault is created with this sfScale value. Useful for
155 // tests that need finer loanScale to exercise rounding edge cases.
157 std::nullopt; // NOLINT(readability-redundant-member-init)
158
159 [[nodiscard]] Number
160 maxCoveredLoanValue(Number const& currentDebt) const
161 {
163 auto debtLimit = coverDeposit * kTenthBipsPerUnity.value() / coverRateMin.value();
164
165 return debtLimit - currentDebt;
166 }
167
168 static BrokerParameters const&
170 {
171 static BrokerParameters const kResult{};
172 return kResult;
173 }
174
175 // TODO: create an operator() which returns a transaction similar to
176 // LoanParameters
177 };
178
180 {
186 jtx::PrettyAsset const& asset,
187 Keylet const& brokerKeylet,
188 Keylet const& vaultKeylet,
190 : asset(asset)
192 , vaultID(vaultKeylet.key)
193 , params(std::move(p))
194 {
195 }
196
197 [[nodiscard]] Keylet
199 {
201 }
202 [[nodiscard]] Keylet
204 {
205 return keylet::vault(vaultID);
206 }
207
208 [[nodiscard]] int
209 vaultScale(jtx::Env const& env) const
210 {
211 using namespace jtx;
212
213 auto const vaultSle = env.le(keylet::vault(vaultID));
214 return getAssetsTotalScale(vaultSle);
215 }
216 };
217
219 {
220 // The account submitting the transaction. May be borrower or broker.
222 // The counterparty. Should be the other of borrower or broker.
224 // Whether the counterparty is specified in the `counterparty` field, or
225 // only signs.
228 // NOLINTBEGIN(readability-redundant-member-init)
243 // NOLINTEND(readability-redundant-member-init)
244
245 template <class... FN>
247 operator()(jtx::Env& env, BrokerInfo const& broker, FN const&... fN) const
248 {
249 using namespace jtx;
250 using namespace jtx::loan;
251
252 JTx jt{loan::set(
253 account,
254 broker.brokerID,
255 broker.asset(principalRequest).number(),
256 flags.value_or(0))};
257
258 Sig(sfCounterpartySignature, counter)(env, jt);
259
260 Fee{setFee.value_or(env.current()->fees().base * 2)}(env, jt);
261
263 kCounterparty(counter)(env, jt);
264 if (originationFee)
265 kLoanOriginationFee(broker.asset(*originationFee).number())(env, jt);
266 if (serviceFee)
267 kLoanServiceFee(broker.asset(*serviceFee).number())(env, jt);
268 if (lateFee)
269 kLatePaymentFee(broker.asset(*lateFee).number())(env, jt);
270 if (closeFee)
271 kClosePaymentFee(broker.asset(*closeFee).number())(env, jt);
272 if (overFee)
273 kOverpaymentFee (*overFee)(env, jt);
274 if (interest)
275 kInterestRate (*interest)(env, jt);
276 if (lateInterest)
277 kLateInterestRate (*lateInterest)(env, jt);
278 if (closeInterest)
279 kCloseInterestRate (*closeInterest)(env, jt);
281 kOverpaymentInterestRate (*overpaymentInterest)(env, jt);
282 if (payTotal)
283 kPaymentTotal (*payTotal)(env, jt);
284 if (payInterval)
285 kPaymentInterval (*payInterval)(env, jt);
286 if (gracePd)
287 kGracePeriod (*gracePd)(env, jt);
288
289 return env.jt(jt, fN...);
290 }
291 };
292
294 {
298 bool showStepBalances = false;
299 bool validateBalances = true;
300
301 static PaymentParameters const&
303 {
304 static PaymentParameters const kResult{};
305 return kResult;
306 }
307 };
308
324
329 {
330 public:
331 jtx::Env const& env;
335
337 jtx::Env const& env,
338 BrokerInfo const& broker,
339 jtx::Account const& pseudo,
340 Keylet const& keylet)
342 {
343 }
344
347 void
349 Number const& principalOutstanding,
350 Number const& interestOwed,
351 TenthBips32 interestRate,
352 std::uint32_t paymentInterval,
353 std::uint32_t paymentsRemaining,
355 {
356 using namespace jtx;
357 if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
358 env.test.BEAST_EXPECT(brokerSle))
359 {
360 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
361 auto const brokerDebt = brokerSle->at(sfDebtTotal);
362 auto const expectedDebt = principalOutstanding + interestOwed;
363 env.test.BEAST_EXPECT(brokerDebt == expectedDebt);
364 env.test.BEAST_EXPECT(
365 env.balance(pseudoAccount, broker.asset).number() ==
366 brokerSle->at(sfCoverAvailable));
367 env.test.BEAST_EXPECT(brokerSle->at(sfOwnerCount) == ownerCount);
368
369 if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
370 env.test.BEAST_EXPECT(vaultSle))
371 {
372 Account const vaultPseudo{"vaultPseudoAccount", vaultSle->at(sfAccount)};
373 env.test.BEAST_EXPECT(
374 vaultSle->at(sfAssetsAvailable) ==
375 env.balance(vaultPseudo, broker.asset).number());
376 if (ownerCount == 0)
377 {
378 // The Vault must be perfectly balanced if there
379 // are no loans outstanding
380 auto const total = vaultSle->at(sfAssetsTotal);
381 auto const available = vaultSle->at(sfAssetsAvailable);
382 env.test.BEAST_EXPECT(total == available);
383 env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
384 }
385 }
386 }
387 }
388
389 void
391 std::int32_t loanScale,
392 jtx::Account const& account,
393 jtx::PrettyAmount const& balanceBefore,
394 STAmount const& expectedPayment,
395 jtx::PrettyAmount const& adjustment) const
396 {
397 auto const borrowerScale = std::max(loanScale, balanceBefore.number().exponent());
398
399 STAmount const balanceChangeAmount{
400 broker.asset,
401 roundToAsset(broker.asset, expectedPayment + adjustment, borrowerScale)};
402 {
403 auto const difference = roundToScale(
404 env.balance(account, broker.asset) - (balanceBefore - balanceChangeAmount),
405 borrowerScale);
406 env.test.expect(
407 roundToScale(difference, loanScale) >= beast::kZero,
408 "Balance before: " + to_string(balanceBefore.value()) +
409 ", expected change: " + to_string(balanceChangeAmount) +
410 ", difference (balance after - expected): " + to_string(difference),
411 __FILE__,
412 __LINE__);
413 }
414 }
415
417 void
419 std::uint32_t previousPaymentDate,
420 std::uint32_t nextPaymentDate,
421 std::uint32_t paymentRemaining,
422 Number const& loanScale,
423 Number const& totalValue,
424 Number const& principalOutstanding,
425 Number const& managementFeeOutstanding,
426 Number const& periodicPayment,
427 std::uint32_t flags) const
428 {
429 using namespace jtx;
430 if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
431 {
432 env.test.BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == previousPaymentDate);
433 env.test.BEAST_EXPECT(loan->at(sfPaymentRemaining) == paymentRemaining);
434 env.test.BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == nextPaymentDate);
435 env.test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale);
436 env.test.BEAST_EXPECT(loan->at(sfTotalValueOutstanding) == totalValue);
437 env.test.BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalOutstanding);
438 env.test.BEAST_EXPECT(
439 loan->at(sfManagementFeeOutstanding) == managementFeeOutstanding);
440 env.test.BEAST_EXPECT(loan->at(sfPeriodicPayment) == periodicPayment);
441 env.test.BEAST_EXPECT(loan->at(sfFlags) == flags);
442
443 auto const ls = constructRoundedLoanState(loan);
444
445 auto const interestRate = TenthBips32{loan->at(sfInterestRate)};
446 auto const paymentInterval = loan->at(sfPaymentInterval);
448 principalOutstanding,
449 ls.interestDue,
450 interestRate,
451 paymentInterval,
452 paymentRemaining,
453 1);
454
455 if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
456 env.test.BEAST_EXPECT(brokerSle))
457 {
458 if (auto vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
459 env.test.BEAST_EXPECT(vaultSle))
460 {
461 if (((flags & lsfLoanImpaired) != 0u) && ((flags & lsfLoanDefault) == 0u))
462 {
463 env.test.BEAST_EXPECT(
464 vaultSle->at(sfLossUnrealized) ==
465 totalValue - managementFeeOutstanding);
466 }
467 else
468 {
469 env.test.BEAST_EXPECT(vaultSle->at(sfLossUnrealized) == 0);
470 }
471 }
472 }
473 }
474 }
475
477 void
478 operator()(LoanState const& state) const
479 {
482 state.nextPaymentDate,
483 state.paymentRemaining,
484 state.loanScale,
485 state.totalValue,
488 state.periodicPayment,
489 state.flags);
490 };
491 };
492
493 BrokerInfo
495 jtx::Env& env,
496 jtx::PrettyAsset const& asset,
497 jtx::Account const& lender,
499 {
500 using namespace jtx;
501
502 Vault const vault{env};
503
504 auto const deposit = asset(params.vaultDeposit);
505 auto const debtMaximumValue = asset(params.debtMax).value();
506 auto const coverDepositValue = asset(params.coverDeposit).value();
507
508 auto const coverRateMinValue = params.coverRateMin;
509
510 auto [tx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
511 if (params.vaultScale)
512 tx[sfScale] = *params.vaultScale;
513 env(tx);
514 env.close();
515 BEAST_EXPECT(env.le(vaultKeylet));
516
517 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
518 env.close();
519 if (auto const vault = env.le(keylet::vault(vaultKeylet.key)); BEAST_EXPECT(vault))
520 {
521 BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value());
522 }
523
524 auto const keylet = keylet::loanBroker(lender.id(), env.seq(lender));
525
526 using namespace loanBroker;
527 env(set(lender, vaultKeylet.key, params.flags),
528 kData(params.data),
529 kManagementFeeRate(params.managementFeeRate),
530 kDebtMaximum(debtMaximumValue),
531 kCoverRateMinimum(coverRateMinValue),
532 kCoverRateLiquidation(TenthBips32(params.coverRateLiquidation)));
533
534 if (coverDepositValue != beast::kZero)
535 env(coverDeposit(lender, keylet.key, coverDepositValue));
536
537 env.close();
538
539 return {asset, keylet, vaultKeylet, params};
540 }
541
544 getCurrentState(jtx::Env const& env, BrokerInfo const& broker, Keylet const& loanKeylet)
545 {
546 using d = NetClock::duration;
547 using tp = NetClock::time_point;
548
549 // Lookup the current loan state
550 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
551 {
552 return LoanState{
553 .previousPaymentDate = loan->at(sfPreviousPaymentDueDate),
554 .startDate = tp{d{loan->at(sfStartDate)}},
555 .nextPaymentDate = loan->at(sfNextPaymentDueDate),
556 .paymentRemaining = loan->at(sfPaymentRemaining),
557 .loanScale = loan->at(sfLoanScale),
558 .totalValue = loan->at(sfTotalValueOutstanding),
559 .principalOutstanding = loan->at(sfPrincipalOutstanding),
560 .managementFeeOutstanding = loan->at(sfManagementFeeOutstanding),
561 .periodicPayment = loan->at(sfPeriodicPayment),
562 .flags = loan->at(sfFlags),
563 .paymentInterval = loan->at(sfPaymentInterval),
564 .interestRate = TenthBips32{loan->at(sfInterestRate)},
565 };
566 }
567 return LoanState{};
568 }
569
574 jtx::Env const& env,
575 BrokerInfo const& broker,
576 Keylet const& loanKeylet,
577 VerifyLoanStatus const& verifyLoanStatus)
578 {
579 using namespace std::chrono_literals;
580 using d = NetClock::duration;
581 using tp = NetClock::time_point;
582
583 auto const state = getCurrentState(env, broker, loanKeylet);
584 BEAST_EXPECT(state.previousPaymentDate == 0);
585 BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s);
586 BEAST_EXPECT(state.paymentRemaining == 12);
587 BEAST_EXPECT(state.principalOutstanding == broker.asset(1000).value());
588 BEAST_EXPECT(
589 state.loanScale >=
590 (broker.asset.integral()
591 ? 0
592 : std::max(broker.vaultScale(env), state.principalOutstanding.exponent())));
593 BEAST_EXPECT(state.paymentInterval == 600);
594 {
596 BEAST_EXPECT(
597 state.totalValue ==
599 broker.asset, state.periodicPayment * state.paymentRemaining, state.loanScale));
600 }
601 BEAST_EXPECT(
602 state.managementFeeOutstanding ==
604 broker.asset,
605 state.totalValue - state.principalOutstanding,
607 state.loanScale));
608
609 verifyLoanStatus(state);
610
611 return state;
612 }
613
614 bool
615 canImpairLoan(jtx::Env const& env, BrokerInfo const& broker, LoanState const& state)
616 {
617 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
618 BEAST_EXPECT(brokerSle))
619 {
620 if (auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
621 BEAST_EXPECT(vaultSle))
622 {
623 // log << vaultSle->getJson() << std::endl;
624 auto const assetsUnavailable =
625 vaultSle->at(sfAssetsTotal) - vaultSle->at(sfAssetsAvailable);
626 auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + state.totalValue -
628
629 if (!BEAST_EXPECT(unrealizedLoss <= assetsUnavailable))
630 {
631 return false;
632 }
633 }
634 }
635 return true;
636 }
637
638 enum class AssetType { XRP = 0, IOU = 1, MPT = 2 };
639
640 // Specify the accounts as params to allow other accounts to be used
643 jtx::Env& env,
644 AssetType assetType,
645 BrokerParameters const& brokerParams,
646 jtx::Account const& issuer,
647 jtx::Account const& lender,
648 jtx::Account const& borrower)
649 {
650 using namespace jtx;
651
652 switch (assetType)
653 {
654 case AssetType::XRP:
655 // TODO: remove the factor, and set up loans in drops
656 return PrettyAsset{xrpIssue(), 1'000'000};
657
658 case AssetType::IOU: {
659 PrettyAsset const asset{issuer[iouCurrency_]};
660
661 auto const limit =
662 asset(100 * (brokerParams.vaultDeposit + brokerParams.coverDeposit));
663 if (lender != issuer)
664 env(trust(lender, limit));
665 if (borrower != issuer)
666 env(trust(borrower, limit));
667
668 return asset;
669 }
670
671 case AssetType::MPT: {
672 // Enough to cover initial fees
673 if (!env.le(keylet::account(issuer)))
674 env.fund(env.current()->fees().accountReserve(10) * 10, issuer);
675 if (!env.le(keylet::account(lender)))
676 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender));
677 if (!env.le(keylet::account(borrower)))
678 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower));
679
680 MPTTester mptt{env, issuer, kMptInitNoFund};
681 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
682 // Scale the MPT asset so interest is interesting
683 PrettyAsset const asset{mptt.issuanceID(), 10'000};
684 // Need to do the authorization here because mptt isn't
685 // accessible outside
686 if (lender != issuer)
687 mptt.authorize({.account = lender});
688 if (borrower != issuer)
689 mptt.authorize({.account = borrower});
690
691 env.close();
692
693 return asset;
694 }
695
696 default:
697 throw std::runtime_error("Unknown asset type");
698 }
699 }
700
701 void
703 jtx::Env& env,
704 BrokerParameters const& brokerParams,
705 LoanParameters const& loanParams,
706 AssetType assetType,
707 jtx::Account const& issuer,
708 jtx::Account const& lender,
709 jtx::Account const& borrower)
710 {
711 using namespace jtx;
712
713 auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower);
714 auto const principal = asset(loanParams.principalRequest).number();
715 auto const interest = loanParams.interest.value_or(TenthBips32{});
716 auto const interval = loanParams.payInterval.value_or(LoanSet::kDefaultPaymentInterval);
717 auto const total = loanParams.payTotal.value_or(LoanSet::kDefaultPaymentTotal);
718 auto const feeRate = brokerParams.managementFeeRate;
719 auto const props = computeLoanProperties(
720 env.current()->rules(),
721 asset,
722 principal,
723 interest,
724 interval,
725 total,
726 feeRate,
727 asset(brokerParams.vaultDeposit).number().exponent());
728 log << "Loan properties:\n"
729 << "\tPrincipal: " << principal << std::endl
730 << "\tInterest rate: " << interest << std::endl
731 << "\tPayment interval: " << interval << std::endl
732 << "\tManagement Fee Rate: " << feeRate << std::endl
733 << "\tTotal Payments: " << total << std::endl
734 << "\tPeriodic Payment: " << props.periodicPayment << std::endl
735 << "\tTotal Value: " << props.loanState.valueOutstanding << std::endl
736 << "\tManagement Fee: " << props.loanState.managementFeeDue << std::endl
737 << "\tLoan Scale: " << props.loanScale << std::endl
738 << "\tFirst payment principal: " << props.firstPaymentPrincipal << std::endl;
739
740 // checkGuards returns a TER, so success is 0
741 BEAST_EXPECT(!checkLoanGuards(
742 asset,
743 asset(loanParams.principalRequest).number(),
744 loanParams.interest.value_or(TenthBips32{}) != beast::kZero,
746 props,
747 env.journal));
748 }
749
752 jtx::Env& env,
753 AssetType assetType,
754 BrokerParameters const& brokerParams,
755 LoanParameters const& loanParams,
756 jtx::Account const& issuer,
757 jtx::Account const& lender,
758 jtx::Account const& borrower)
759 {
760 using namespace jtx;
761
762 // Enough to cover initial fees
763 env.fund(env.current()->fees().accountReserve(10) * 10, issuer);
764 if (lender != issuer)
765 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(lender));
766 if (borrower != issuer && borrower != lender)
767 env.fund(env.current()->fees().accountReserve(10) * 10, noripple(borrower));
768
769 describeLoan(env, brokerParams, loanParams, assetType, issuer, lender, borrower);
770
771 // Make the asset
772 auto const asset = createAsset(env, assetType, brokerParams, issuer, lender, borrower);
773
774 env.close();
775 if (asset.native() || lender != issuer)
776 {
777 env(
778 pay((asset.native() ? env.master : issuer),
779 lender,
780 asset(brokerParams.vaultDeposit + brokerParams.coverDeposit)));
781 }
782 // Fund the borrower later once we know the total loan
783 // size
784
785 BrokerInfo const broker = createVaultAndBroker(env, asset, lender, brokerParams);
786
787 auto const pseudoAcctOpt = [&]() -> std::optional<Account> {
788 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
789 if (!BEAST_EXPECT(brokerSle))
790 return std::nullopt;
791 auto const brokerPseudo = brokerSle->at(sfAccount);
792 return Account("Broker pseudo-account", brokerPseudo);
793 }();
794 if (!pseudoAcctOpt)
795 return std::nullopt;
796 Account const& pseudoAcct = *pseudoAcctOpt;
797
798 auto const loanKeyletOpt = [&]() -> std::optional<Keylet> {
799 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
800 if (!BEAST_EXPECT(brokerSle))
801 return std::nullopt;
802
803 // Broker has no loans
804 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
805
806 // The loan keylet is based on the LoanSequence of the
807 // _LOAN_BROKER_ object.
808 auto const loanSequence = brokerSle->at(sfLoanSequence);
809 return keylet::loan(broker.brokerID, loanSequence);
810 }();
811 if (!loanKeyletOpt)
812 return std::nullopt;
813 Keylet const& loanKeylet = *loanKeyletOpt;
814
815 env(loanParams(env, broker));
816
817 env.close();
818
819 return std::make_tuple(broker, loanKeylet, pseudoAcct);
820 }
821
822 static void
824 jtx::Env& env,
825 BrokerInfo const& broker,
826 jtx::Account const& issuer,
827 jtx::Account const& borrower,
828 LoanState const& state,
829 std::optional<Number> const& servFee)
830 {
831 using namespace jtx;
832
833 STAmount const serviceFee = broker.asset(servFee.value_or(0));
834
835 // Ensure the borrower has enough funds to make the payments
836 // (including tx fees, if necessary)
837 auto const borrowerBalance = env.balance(borrower, broker.asset);
838
839 auto const baseFee = env.current()->fees().base;
840
841 // Add extra for transaction fees and reserves, if appropriate, or a
842 // tiny amount for the extra paid in each transaction
843 auto const totalNeeded = state.totalValue + (serviceFee * state.paymentRemaining) +
844 (broker.asset.native()
845 ? Number(
846 baseFee * state.paymentRemaining +
847 env.current()->fees().accountReserve(env.ownerCount(borrower)))
848 : broker.asset(15).number());
849
850 auto const shortage = totalNeeded - borrowerBalance.number();
851
852 if (shortage > beast::kZero && (broker.asset.native() || issuer != borrower))
853 {
854 env(
855 pay((broker.asset.native() ? env.master : issuer),
856 borrower,
857 STAmount{broker.asset, shortage}));
858 }
859 }
860
861 void
863 jtx::Env& env,
864 BrokerInfo const& broker,
865 LoanParameters const& loanParams,
866 Keylet const& loanKeylet,
867 VerifyLoanStatus const& verifyLoanStatus,
868 jtx::Account const& issuer,
869 jtx::Account const& lender,
870 jtx::Account const& borrower,
871 PaymentParameters const& paymentParams = PaymentParameters::defaults())
872 {
873 // Make all the individual payments
874 using namespace jtx;
875 using namespace jtx::loan;
876 using namespace std::chrono_literals;
877 using d = NetClock::duration;
878
879 bool const showStepBalances = paymentParams.showStepBalances;
880
881 auto const currencyLabel = getCurrencyLabel(broker.asset);
882
883 auto const baseFee = env.current()->fees().base;
884
885 env.close();
886 auto state = getCurrentState(env, broker, loanKeylet);
887
888 verifyLoanStatus(state);
889
890 STAmount const serviceFee = broker.asset(loanParams.serviceFee.value_or(0));
891
892 topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee);
893
894 // Periodic payment amount will consist of
895 // 1. principal outstanding (1000)
896 // 2. interest interest rate (at 12%)
897 // 3. payment interval (600s)
898 // 4. loan service fee (2)
899 // Calculate these values without the helper functions
900 // to verify they're working correctly The numbers in
901 // the below BEAST_EXPECTs may not hold across assets.
902 auto const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval);
903 STAmount const roundedPeriodicPayment{
904 broker.asset,
905 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
906
907 if (!showStepBalances)
908 {
909 log << currencyLabel << " Payment components: "
910 << "Payments remaining, "
911 << "rawInterest, rawPrincipal, "
912 "rawMFee, "
913 << "trackedValueDelta, trackedPrincipalDelta, "
914 "trackedInterestDelta, trackedMgmtFeeDelta, special"
915 << std::endl;
916 }
917
918 // Include the service fee
919 STAmount const totalDue = roundToScale(
920 roundedPeriodicPayment + serviceFee, state.loanScale, Number::RoundingMode::Upward);
921
922 auto currentRoundedState = constructLoanState(
923 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
924 {
925 auto const raw = computeTheoreticalLoanState(
926 env.current()->rules(),
927 state.periodicPayment,
928 periodicRate,
929 state.paymentRemaining,
931
932 if (showStepBalances)
933 {
934 log << currencyLabel << " Starting loan balances: "
935 << "\n\tTotal value: " << currentRoundedState.valueOutstanding
936 << "\n\tPrincipal: " << currentRoundedState.principalOutstanding
937 << "\n\tInterest: " << currentRoundedState.interestDue
938 << "\n\tMgmt fee: " << currentRoundedState.managementFeeDue
939 << "\n\tPayments remaining " << state.paymentRemaining << std::endl;
940 }
941 else
942 {
943 log << currencyLabel << " Loan starting state: " << state.paymentRemaining << ", "
944 << raw.interestDue << ", " << raw.principalOutstanding << ", "
945 << raw.managementFeeDue << ", " << currentRoundedState.valueOutstanding << ", "
946 << currentRoundedState.principalOutstanding << ", "
947 << currentRoundedState.interestDue << ", "
948 << currentRoundedState.managementFeeDue << std::endl;
949 }
950 }
951
952 // Try to pay a little extra to show that it's _not_
953 // taken
954 auto const extraAmount = paymentParams.overpaymentExtra
955 ? broker.asset(*paymentParams.overpaymentExtra).value()
956 : std::min(broker.asset(10).value(), STAmount{broker.asset, totalDue / 20});
957
958 STAmount const transactionAmount =
959 STAmount{broker.asset, totalDue * paymentParams.overpaymentFactor} + extraAmount;
960
961 auto const borrowerInitialBalance = env.balance(borrower, broker.asset).number();
962 auto const initialState = state;
964 .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0};
965 Number totalInterestPaid = 0;
966 Number totalFeesPaid = 0;
967 std::size_t totalPaymentsMade = 0;
968
970 env.current()->rules(),
971 state.periodicPayment,
972 periodicRate,
973 state.paymentRemaining,
975
976 auto validateBorrowerBalance = [&]() {
977 if (borrower == issuer || !paymentParams.validateBalances)
978 return;
979 auto const totalSpent =
980 (totalPaid.trackedValueDelta + totalFeesPaid +
981 (broker.asset.native() ? Number(baseFee) * totalPaymentsMade : kNumZero));
982 BEAST_EXPECT(
983 env.balance(borrower, broker.asset).number() ==
984 borrowerInitialBalance - totalSpent);
985 };
986
987 auto const defaultRound = broker.asset.integral() ? 3 : 0;
988 auto truncate = [defaultRound](Number const& n, std::optional<int> places = std::nullopt) {
989 auto const p = places.value_or(defaultRound);
990 if (p == 0)
991 return n;
992 auto const factor = Number{1, p};
993 return (n * factor).truncate() / factor;
994 };
995 while (state.paymentRemaining > 0)
996 {
997 validateBorrowerBalance();
998 // Compute the expected principal amount
999 auto const paymentComponents = xrpl::detail::computePaymentComponents(
1000 env.current()->rules(),
1001 broker.asset.raw(),
1002 state.loanScale,
1003 state.totalValue,
1004 state.principalOutstanding,
1005 state.managementFeeOutstanding,
1006 state.periodicPayment,
1007 periodicRate,
1008 state.paymentRemaining,
1009 broker.params.managementFeeRate);
1010
1011 BEAST_EXPECT(
1012 paymentComponents.trackedValueDelta <= roundedPeriodicPayment ||
1013 (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final &&
1014 paymentComponents.trackedValueDelta >= roundedPeriodicPayment));
1015 BEAST_EXPECT(
1016 paymentComponents.trackedValueDelta ==
1017 paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() +
1018 paymentComponents.trackedManagementFeeDelta);
1019
1020 xrpl::LoanState const nextTrueState = computeTheoreticalLoanState(
1021 env.current()->rules(),
1022 state.periodicPayment,
1023 periodicRate,
1024 state.paymentRemaining - 1,
1025 broker.params.managementFeeRate);
1026 xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState;
1027 BEAST_EXPECT(
1028 deltas.total() == deltas.principal + deltas.interest + deltas.managementFee);
1029 BEAST_EXPECT(
1030 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
1031 deltas.total() == state.periodicPayment ||
1032 (state.loanScale - (deltas.total() - state.periodicPayment).exponent()) > 14);
1033
1034 if (!showStepBalances)
1035 {
1036 log << currencyLabel << " Payment components: " << state.paymentRemaining << ", "
1037
1038 << deltas.interest << ", " << deltas.principal << ", " << deltas.managementFee
1039 << ", " << paymentComponents.trackedValueDelta << ", "
1040 << paymentComponents.trackedPrincipalDelta << ", "
1041 << paymentComponents.trackedInterestPart() << ", "
1042 << paymentComponents.trackedManagementFeeDelta << ", " << [&]() -> char const* {
1043 if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Final)
1044 return "final";
1045 if (paymentComponents.specialCase == ::xrpl::detail::PaymentSpecialCase::Extra)
1046 return "extra";
1047 return "none";
1048 }() << std::endl;
1049 }
1050
1051 auto const totalDueAmount =
1052 STAmount{broker.asset, paymentComponents.trackedValueDelta + serviceFee};
1053
1054 if (paymentParams.validateBalances)
1055 {
1056 // Due to the rounding algorithms to keep the interest and
1057 // principal in sync with "true" values, the computed amount
1058 // may be a little less than the rounded fixed payment
1059 // amount. For integral types, the difference should be < 3
1060 // (1 unit for each of the interest and management fee). For
1061 // IOUs, the difference should be dust.
1062 Number const diff = totalDue - totalDueAmount;
1063 BEAST_EXPECT(
1064 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
1065 diff == beast::kZero ||
1066 (diff > beast::kZero &&
1067 ((broker.asset.integral() && (static_cast<Number>(diff) < 3)) ||
1068 (state.loanScale - diff.exponent() > 13))));
1069
1070 BEAST_EXPECT(
1071 paymentComponents.trackedPrincipalDelta >= beast::kZero &&
1072 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
1073 BEAST_EXPECT(
1074 paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final ||
1075 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
1076 }
1077
1078 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
1079
1080 // Make the payment
1081 env(pay(borrower, loanKeylet.key, transactionAmount, paymentParams.flags));
1082
1083 env.close(d{state.paymentInterval / 2});
1084
1085 if (paymentParams.validateBalances)
1086 {
1087 // Need to account for fees if the loan is in XRP
1088 PrettyAmount adjustment = broker.asset(0);
1089 if (broker.asset.native())
1090 {
1091 adjustment = env.current()->fees().base;
1092 }
1093
1094 // Check the result
1095 verifyLoanStatus.checkPayment(
1096 state.loanScale,
1097 borrower,
1098 borrowerBalanceBeforePayment,
1099 totalDueAmount,
1100 adjustment);
1101 }
1102
1103 if (showStepBalances)
1104 {
1105 auto const loanSle = env.le(loanKeylet);
1106 if (!BEAST_EXPECT(loanSle))
1107 {
1108 // No reason for this not to exist
1109 return;
1110 }
1111 auto const current = constructRoundedLoanState(loanSle);
1112 auto const errors = nextTrueState - current;
1113 log << currencyLabel << " Loan balances: "
1114 << "\n\tAmount taken: " << paymentComponents.trackedValueDelta
1115 << "\n\tTotal value: " << current.valueOutstanding
1116 << " (true: " << truncate(nextTrueState.valueOutstanding)
1117 << ", error: " << truncate(errors.total())
1118 << ")\n\tPrincipal: " << current.principalOutstanding
1119 << " (true: " << truncate(nextTrueState.principalOutstanding)
1120 << ", error: " << truncate(errors.principal)
1121 << ")\n\tInterest: " << current.interestDue
1122 << " (true: " << truncate(nextTrueState.interestDue)
1123 << ", error: " << truncate(errors.interest)
1124 << ")\n\tMgmt fee: " << current.managementFeeDue
1125 << " (true: " << truncate(nextTrueState.managementFeeDue)
1126 << ", error: " << truncate(errors.managementFee) << ")\n\tPayments remaining "
1127 << loanSle->at(sfPaymentRemaining) << std::endl;
1128
1129 currentRoundedState = current;
1130 }
1131
1132 --state.paymentRemaining;
1133 state.previousPaymentDate = state.nextPaymentDate;
1134 if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final)
1135 {
1136 state.paymentRemaining = 0;
1137 state.nextPaymentDate = 0;
1138 }
1139 else
1140 {
1141 state.nextPaymentDate += state.paymentInterval;
1142 }
1143 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
1144 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
1145 state.totalValue -= paymentComponents.trackedValueDelta;
1146
1147 if (paymentParams.validateBalances)
1148 verifyLoanStatus(state);
1149
1150 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
1151 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
1152 totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta;
1153 totalInterestPaid += paymentComponents.trackedInterestPart();
1154 totalFeesPaid += serviceFee;
1155 ++totalPaymentsMade;
1156
1157 currentTrueState = nextTrueState;
1158 }
1159 validateBorrowerBalance();
1160
1161 // Loan is paid off
1162 BEAST_EXPECT(state.paymentRemaining == 0);
1163 BEAST_EXPECT(state.principalOutstanding == 0);
1164
1165 auto const initialInterestDue = initialState.totalValue -
1166 (initialState.principalOutstanding + initialState.managementFeeOutstanding);
1167 if (paymentParams.validateBalances)
1168 {
1169 // Make sure all the payments add up
1170 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
1171 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
1172 BEAST_EXPECT(
1173 totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
1174 // This is almost a tautology given the previous checks, but
1175 // check it anyway for completeness.
1176 BEAST_EXPECT(totalInterestPaid == initialInterestDue);
1177 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
1178 }
1179
1180 if (showStepBalances)
1181 {
1182 auto const loanSle = env.le(loanKeylet);
1183 if (!BEAST_EXPECT(loanSle))
1184 {
1185 // No reason for this not to exist
1186 return;
1187 }
1188 log << currencyLabel << " Total amounts paid: "
1189 << "\n\tTotal value: " << totalPaid.trackedValueDelta
1190 << " (initial: " << truncate(initialState.totalValue)
1191 << ", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta)
1192 << ")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta
1193 << " (initial: " << truncate(initialState.principalOutstanding) << ", error: "
1194 << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta)
1195 << ")\n\tInterest: " << totalInterestPaid
1196 << " (initial: " << truncate(initialInterestDue)
1197 << ", error: " << truncate(initialInterestDue - totalInterestPaid)
1198 << ")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta
1199 << " (initial: " << truncate(initialState.managementFeeOutstanding) << ", error: "
1200 << truncate(
1201 initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta)
1202 << ")\n\tTotal payments made: " << totalPaymentsMade << std::endl;
1203 }
1204 }
1205
1206 void
1208 AssetType assetType,
1209 BrokerParameters const& brokerParams,
1210 LoanParameters const& loanParams,
1211 FeatureBitset features)
1212 {
1213 using namespace jtx;
1214
1215 Account const issuer("issuer");
1216 Account const lender("lender");
1217 Account const borrower("borrower");
1218
1219 Env env(*this, features);
1220
1221 auto loanResult =
1222 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
1223 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
1224 return;
1225
1226 auto broker = std::get<BrokerInfo>(*loanResult);
1227 auto loanKeylet = std::get<Keylet>(*loanResult);
1228 auto pseudoAcct = std::get<Account>(*loanResult);
1229
1230 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
1231
1233 env,
1234 broker,
1235 loanParams,
1236 loanKeylet,
1237 verifyLoanStatus,
1238 issuer,
1239 lender,
1240 borrower,
1241 PaymentParameters{.showStepBalances = true});
1242 }
1243
1254 void
1256 std::string const& caseLabel,
1257 char const* label,
1258 jtx::Env& env,
1259 Number const& loanAmount,
1260 int interestExponent,
1261 jtx::Account const& lender,
1262 jtx::Account const& borrower,
1263 jtx::Account const& evan,
1264 BrokerInfo const& broker,
1265 jtx::Account const& pseudoAcct,
1266 std::uint32_t flags,
1267 // The end of life callback is expected to take the loan to 0 payments
1268 // remaining, one way or another
1269 std::function<void(Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus)>
1270 toEndOfLife)
1271 {
1272 auto const [keylet, loanSequence] = [&]() {
1273 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1274 if (!BEAST_EXPECT(brokerSle))
1275 {
1276 // will be invalid
1278 }
1279
1280 // Broker has no loans
1281 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1282
1283 // The loan keylet is based on the LoanSequence of the _LOAN_BROKER_
1284 // object.
1285 auto const loanSequence = brokerSle->at(sfLoanSequence);
1286 return std::make_pair(keylet::loan(broker.brokerID, loanSequence), loanSequence);
1287 }();
1288
1289 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet);
1290
1291 // No loans yet
1292 verifyLoanStatus.checkBroker(0, 0, TenthBips32{0}, 1, 0, 0);
1293
1294 if (!BEAST_EXPECT(loanSequence != 0))
1295 return;
1296
1297 testcase << caseLabel << " " << label;
1298
1299 using namespace jtx;
1300 using namespace loan;
1301 using namespace std::chrono_literals;
1302
1303 auto applyExponent = [interestExponent, this](TenthBips32 value) mutable {
1304 BEAST_EXPECT(value > TenthBips32(0));
1305 while (interestExponent > 0)
1306 {
1307 auto const oldValue = value;
1308 value *= 10;
1309 --interestExponent;
1310 BEAST_EXPECT(value / 10 == oldValue);
1311 }
1312 while (interestExponent < 0)
1313 {
1314 auto const oldValue = value;
1315 value /= 10;
1316 ++interestExponent;
1317 BEAST_EXPECT(value * 10 == oldValue);
1318 }
1319 return value;
1320 };
1321
1322 auto const borrowerOwnerCount = env.ownerCount(borrower);
1323
1324 auto const loanSetFee = env.current()->fees().base * 2;
1325 LoanParameters const loanParams{
1326 .account = borrower,
1327 .counter = lender,
1328 .counterpartyExplicit = false,
1329 .principalRequest = loanAmount,
1330 .setFee = loanSetFee,
1331 .originationFee = 1,
1332 .serviceFee = 2,
1333 .lateFee = 3,
1334 .closeFee = 4,
1335 .overFee = applyExponent(percentageToTenthBips(5) / 10),
1336 .interest = applyExponent(percentageToTenthBips(12)),
1337 // 2.4%
1338 .lateInterest = applyExponent(percentageToTenthBips(24) / 10),
1339 .closeInterest = applyExponent(percentageToTenthBips(36) / 10),
1340 .overpaymentInterest = applyExponent(percentageToTenthBips(48) / 10),
1341 .payTotal = 12,
1342 .payInterval = 600,
1343 .gracePd = 60,
1344 .flags = flags,
1345 };
1346 Number const principalRequestAmount = broker.asset(loanParams.principalRequest).value();
1347 auto const originationFeeAmount = broker.asset(*loanParams.originationFee).value();
1348 auto const serviceFeeAmount = broker.asset(*loanParams.serviceFee).value();
1349 auto const lateFeeAmount = broker.asset(*loanParams.lateFee).value();
1350 auto const closeFeeAmount = broker.asset(*loanParams.closeFee).value();
1351
1352 auto const borrowerStartbalance = env.balance(borrower, broker.asset);
1353
1354 auto createJtx = loanParams(env, broker);
1355 // Successfully create a Loan
1356 env(createJtx);
1357
1358 env.close();
1359
1360 auto const startDate = env.current()->header().parentCloseTime.time_since_epoch().count();
1361
1362 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1363 BEAST_EXPECT(brokerSle))
1364 {
1365 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1);
1366 }
1367
1368 {
1369 // Need to account for fees if the loan is in XRP
1370 PrettyAmount adjustment = broker.asset(0);
1371 if (broker.asset.native())
1372 {
1373 adjustment = 2 * env.current()->fees().base;
1374 }
1375
1376 BEAST_EXPECT(
1377 env.balance(borrower, broker.asset).value() ==
1378 borrowerStartbalance.value() + principalRequestAmount - originationFeeAmount -
1379 adjustment.value());
1380 }
1381
1382 auto const loanFlags =
1383 createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0);
1384
1385 if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
1386 {
1387 // log << "loan after create: " << to_string(loan->getJson())
1388 // << std::endl;
1389 BEAST_EXPECT(
1390 loan->isFlag(lsfLoanOverpayment) == createJtx.stx->isFlag(tfLoanOverpayment));
1391 BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence);
1392 BEAST_EXPECT(loan->at(sfBorrower) == borrower.id());
1393 BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.brokerID);
1394 BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount);
1395 BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount);
1396 BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount);
1397 BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount);
1398 BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.overFee);
1399 BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.interest);
1400 BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.lateInterest);
1401 BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.closeInterest);
1402 BEAST_EXPECT(loan->at(sfOverpaymentInterestRate) == *loanParams.overpaymentInterest);
1403 BEAST_EXPECT(loan->at(sfStartDate) == startDate);
1404 BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.payInterval);
1405 BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd);
1406 BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0);
1407 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.payInterval);
1408 BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.payTotal);
1409 BEAST_EXPECT(
1410 loan->at(sfLoanScale) >=
1411 (broker.asset.integral()
1412 ? 0
1413 : std::max(broker.vaultScale(env), principalRequestAmount.exponent())));
1414 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount);
1415 }
1416
1417 auto state = getCurrentState(env, broker, keylet, verifyLoanStatus);
1418
1419 auto const loanProperties = computeLoanProperties(
1420 env.current()->rules(),
1421 broker.asset.raw(),
1422 state.principalOutstanding,
1423 state.interestRate,
1424 state.paymentInterval,
1425 state.paymentRemaining,
1427 state.loanScale);
1428
1429 verifyLoanStatus(
1430 0,
1431 startDate + *loanParams.payInterval,
1432 *loanParams.payTotal,
1433 state.loanScale,
1434 loanProperties.loanState.valueOutstanding,
1435 principalRequestAmount,
1436 loanProperties.loanState.managementFeeDue,
1437 loanProperties.periodicPayment,
1438 loanFlags | 0);
1439
1440 // Manage the loan
1441 // no-op
1442 env(manage(lender, keylet.key, 0));
1443 {
1444 // no flags
1445 auto jt = manage(lender, keylet.key, 0);
1446 jt.removeMember(sfFlags.getName());
1447 env(jt);
1448 }
1449 // Only the lender can manage
1450 env(manage(evan, keylet.key, 0), Ter(tecNO_PERMISSION));
1451 // unknown flags
1452 env(manage(lender, keylet.key, tfLoanManageMask), Ter(temINVALID_FLAG));
1453 // combinations of flags are not allowed
1454 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair), Ter(temINVALID_FLAG));
1455 env(manage(lender, keylet.key, tfLoanImpair | tfLoanDefault), Ter(temINVALID_FLAG));
1456 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanDefault), Ter(temINVALID_FLAG));
1457 env(manage(lender, keylet.key, tfLoanUnimpair | tfLoanImpair | tfLoanDefault),
1459 // invalid loan ID
1460 env(manage(lender, broker.brokerID, tfLoanImpair), Ter(tecNO_ENTRY));
1461 // Loan is unimpaired, can't unimpair it again
1462 env(manage(lender, keylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION));
1463 // Loan is unimpaired, it can go into default, but only after it's past
1464 // due
1465 env(manage(lender, keylet.key, tfLoanDefault), Ter(tecTOO_SOON));
1466
1467 // Check the vault
1468 bool const canImpair = canImpairLoan(env, broker, state);
1469 // Impair the loan, if possible
1470 env(manage(lender, keylet.key, tfLoanImpair),
1471 canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
1472 // Unimpair the loan
1473 env(manage(lender, keylet.key, tfLoanUnimpair),
1474 canImpair ? Ter(tesSUCCESS) : Ter(tecNO_PERMISSION));
1475
1476 auto const nextDueDate = startDate + *loanParams.payInterval;
1477
1478 env.close();
1479
1480 verifyLoanStatus(
1481 0,
1482 nextDueDate,
1483 *loanParams.payTotal,
1484 loanProperties.loanScale,
1485 loanProperties.loanState.valueOutstanding,
1486 principalRequestAmount,
1487 loanProperties.loanState.managementFeeDue,
1488 loanProperties.periodicPayment,
1489 loanFlags | 0);
1490
1491 // Can't delete the loan yet. It has payments remaining.
1492 env(del(lender, keylet.key), Ter(tecHAS_OBLIGATIONS));
1493
1494 if (BEAST_EXPECT(toEndOfLife))
1495 toEndOfLife(keylet, verifyLoanStatus);
1496 env.close();
1497
1498 // Verify the loan is at EOL
1499 if (auto loan = env.le(keylet); BEAST_EXPECT(loan))
1500 {
1501 BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
1502 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
1503 }
1504 auto const borrowerStartingBalance = env.balance(borrower, broker.asset);
1505
1506 // Try to delete the loan broker with an active loan
1507 env(loanBroker::del(lender, broker.brokerID), Ter(tecHAS_OBLIGATIONS));
1508 // Ensure the above tx doesn't get ordered after the LoanDelete and
1509 // delete our broker!
1510 env.close();
1511
1512 // Test failure cases
1513 env(del(lender, keylet.key, tfLoanOverpayment), Ter(temINVALID_FLAG));
1514 env(del(evan, keylet.key), Ter(tecNO_PERMISSION));
1515 env(del(lender, broker.brokerID), Ter(tecNO_ENTRY));
1516
1517 // Delete the loan
1518 // Either the borrower or the lender can delete the loan. Alternate
1519 // between who does it across tests.
1520 static unsigned kDeleteCounter = 0;
1521 auto const deleter = ((++kDeleteCounter % 2) != 0u) ? lender : borrower;
1522 env(del(deleter, keylet.key));
1523 env.close();
1524
1525 PrettyAmount adjustment = broker.asset(0);
1526 if (deleter == borrower)
1527 {
1528 // Need to account for fees if the loan is in XRP
1529 if (broker.asset.native())
1530 {
1531 adjustment = env.current()->fees().base;
1532 }
1533 }
1534
1535 // No loans left
1536 verifyLoanStatus.checkBroker(0, 0, *loanParams.interest, 1, 0, 0);
1537
1538 BEAST_EXPECT(
1539 env.balance(borrower, broker.asset).value() ==
1540 borrowerStartingBalance.value() - adjustment);
1541 BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
1542
1543 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1544 BEAST_EXPECT(brokerSle))
1545 {
1546 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1547 }
1548 }
1549
1550 static std::string
1552 {
1553 if (asset.native())
1554 return "XRP";
1555 if (asset.holds<Issue>())
1556 return "IOU";
1557 if (asset.holds<MPTIssue>())
1558 return "MPT";
1559 return "Unknown";
1560 }
1561
1570 template <class TAsset, std::size_t NAsset>
1571 void
1573 jtx::Env& env,
1574 jtx::MPTTester& mptt,
1575 std::array<TAsset, NAsset> const& assets,
1576 BrokerInfo const& broker,
1577 Number const& loanAmount,
1578 int interestExponent)
1579 {
1580 using namespace jtx;
1581 using namespace Lending;
1582
1583 auto const& asset = broker.asset.raw();
1584 auto const currencyLabel = getCurrencyLabel(asset);
1585 auto const caseLabel = [&]() {
1587 ss << "Lifecycle: " << loanAmount << " " << currencyLabel
1588 << " Scale interest to: " << interestExponent << " ";
1589 return ss.str();
1590 }();
1591 testcase << caseLabel;
1592
1593 using namespace loan;
1594 using namespace std::chrono_literals;
1595 using d = NetClock::duration;
1596 using tp = NetClock::time_point;
1597
1598 Account const issuer{"issuer"};
1599 // For simplicity, lender will be the sole actor for the vault &
1600 // brokers.
1601 Account const lender{"lender"};
1602 // Borrower only wants to borrow
1603 Account const borrower{"borrower"};
1604 // Evan will attempt to be naughty
1605 Account const evan{"evan"};
1606 // Do not fund alice
1607 Account const alice{"alice"};
1608
1609 Number const principalRequest = broker.asset(loanAmount).value();
1610 Number const maxCoveredLoanValue = broker.params.maxCoveredLoanValue(0);
1611 BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10);
1612 Number const maxCoveredLoanRequest = broker.asset(maxCoveredLoanValue).value();
1613 Number const totalVaultRequest = broker.asset(broker.params.vaultDeposit).value();
1614 Number const debtMaximumRequest = broker.asset(broker.params.debtMax).value();
1615
1616 auto const loanSetFee = Fee(env.current()->fees().base * 2);
1617
1618 auto const pseudoAcct = [&]() {
1619 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1620 if (!BEAST_EXPECT(brokerSle))
1621 return Account{lender};
1622 auto const brokerPseudo = brokerSle->at(sfAccount);
1623 return Account("Broker pseudo-account", brokerPseudo);
1624 }();
1625
1626 auto const baseFee = env.current()->fees().base;
1627
1628 auto badKeylet = keylet::vault(lender.id(), env.seq(lender));
1629 // Try some failure cases
1630 // flags are checked first
1631 env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask),
1632 Sig(sfCounterpartySignature, lender),
1633 loanSetFee,
1635
1636 // field length validation
1637 // sfData: good length, bad account
1638 env(set(evan, broker.brokerID, principalRequest),
1639 Sig(sfCounterpartySignature, borrower),
1641 loanSetFee,
1642 Ter(tefBAD_AUTH));
1643 // sfData: too long
1644 env(set(evan, broker.brokerID, principalRequest),
1645 Sig(sfCounterpartySignature, lender),
1647 loanSetFee,
1648 Ter(temINVALID));
1649
1650 // field range validation
1651 // sfOverpaymentFee: good value, bad account
1652 env(set(evan, broker.brokerID, principalRequest),
1653 Sig(sfCounterpartySignature, borrower),
1654 kOverpaymentFee(kMaxOverpaymentFee),
1655 loanSetFee,
1656 Ter(tefBAD_AUTH));
1657 // sfOverpaymentFee: too big
1658 env(set(evan, broker.brokerID, principalRequest),
1659 Sig(sfCounterpartySignature, lender),
1660 kOverpaymentFee(kMaxOverpaymentFee + 1),
1661 loanSetFee,
1662 Ter(temINVALID));
1663
1664 // sfInterestRate: good value, bad account
1665 env(set(evan, broker.brokerID, principalRequest),
1666 Sig(sfCounterpartySignature, borrower),
1667 kInterestRate(kMaxInterestRate),
1668 loanSetFee,
1669 Ter(tefBAD_AUTH));
1670 env(set(evan, broker.brokerID, principalRequest),
1671 Sig(sfCounterpartySignature, borrower),
1672 kInterestRate(TenthBips32(0)),
1673 loanSetFee,
1674 Ter(tefBAD_AUTH));
1675 // sfInterestRate: too big
1676 env(set(evan, broker.brokerID, principalRequest),
1677 Sig(sfCounterpartySignature, lender),
1678 kInterestRate(kMaxInterestRate + 1),
1679 loanSetFee,
1680 Ter(temINVALID));
1681 // sfInterestRate: too small
1682 env(set(evan, broker.brokerID, principalRequest),
1683 Sig(sfCounterpartySignature, lender),
1684 kInterestRate(TenthBips32(-1)),
1685 loanSetFee,
1686 Ter(temINVALID));
1687
1688 // sfLateInterestRate: good value, bad account
1689 env(set(evan, broker.brokerID, principalRequest),
1690 Sig(sfCounterpartySignature, borrower),
1691 kLateInterestRate(kMaxLateInterestRate),
1692 loanSetFee,
1693 Ter(tefBAD_AUTH));
1694 env(set(evan, broker.brokerID, principalRequest),
1695 Sig(sfCounterpartySignature, borrower),
1696 kLateInterestRate(TenthBips32(0)),
1697 loanSetFee,
1698 Ter(tefBAD_AUTH));
1699 // sfLateInterestRate: too big
1700 env(set(evan, broker.brokerID, principalRequest),
1701 Sig(sfCounterpartySignature, lender),
1702 kLateInterestRate(kMaxLateInterestRate + 1),
1703 loanSetFee,
1704 Ter(temINVALID));
1705 // sfLateInterestRate: too small
1706 env(set(evan, broker.brokerID, principalRequest),
1707 Sig(sfCounterpartySignature, lender),
1708 kLateInterestRate(TenthBips32(-1)),
1709 loanSetFee,
1710 Ter(temINVALID));
1711
1712 // sfCloseInterestRate: good value, bad account
1713 env(set(evan, broker.brokerID, principalRequest),
1714 Sig(sfCounterpartySignature, borrower),
1715 kCloseInterestRate(kMaxCloseInterestRate),
1716 loanSetFee,
1717 Ter(tefBAD_AUTH));
1718 env(set(evan, broker.brokerID, principalRequest),
1719 Sig(sfCounterpartySignature, borrower),
1720 kCloseInterestRate(TenthBips32(0)),
1721 loanSetFee,
1722 Ter(tefBAD_AUTH));
1723 // sfCloseInterestRate: too big
1724 env(set(evan, broker.brokerID, principalRequest),
1725 Sig(sfCounterpartySignature, lender),
1726 kCloseInterestRate(kMaxCloseInterestRate + 1),
1727 loanSetFee,
1728 Ter(temINVALID));
1729 env(set(evan, broker.brokerID, principalRequest),
1730 Sig(sfCounterpartySignature, lender),
1731 kCloseInterestRate(TenthBips32(-1)),
1732 loanSetFee,
1733 Ter(temINVALID));
1734
1735 // sfOverpaymentInterestRate: good value, bad account
1736 env(set(evan, broker.brokerID, principalRequest),
1737 Sig(sfCounterpartySignature, borrower),
1738 kOverpaymentInterestRate(kMaxOverpaymentInterestRate),
1739 loanSetFee,
1740 Ter(tefBAD_AUTH));
1741 env(set(evan, broker.brokerID, principalRequest),
1742 Sig(sfCounterpartySignature, borrower),
1743 kOverpaymentInterestRate(TenthBips32(0)),
1744 loanSetFee,
1745 Ter(tefBAD_AUTH));
1746 // sfOverpaymentInterestRate: too big
1747 env(set(evan, broker.brokerID, principalRequest),
1748 Sig(sfCounterpartySignature, lender),
1749 kOverpaymentInterestRate(kMaxOverpaymentInterestRate + 1),
1750 loanSetFee,
1751 Ter(temINVALID));
1752 env(set(evan, broker.brokerID, principalRequest),
1753 Sig(sfCounterpartySignature, lender),
1754 kOverpaymentInterestRate(TenthBips32(-1)),
1755 loanSetFee,
1756 Ter(temINVALID));
1757
1758 // sfPaymentTotal: good value, bad account
1759 env(set(evan, broker.brokerID, principalRequest),
1760 Sig(sfCounterpartySignature, borrower),
1761 kPaymentTotal(LoanSet::kMinPaymentTotal),
1762 loanSetFee,
1763 Ter(tefBAD_AUTH));
1764 // sfPaymentTotal: too small (there is no max)
1765 env(set(evan, broker.brokerID, principalRequest),
1766 Sig(sfCounterpartySignature, lender),
1767 kPaymentTotal(LoanSet::kMinPaymentTotal - 1),
1768 loanSetFee,
1769 Ter(temINVALID));
1770
1771 // sfPaymentInterval: good value, bad account
1772 env(set(evan, broker.brokerID, principalRequest),
1773 Sig(sfCounterpartySignature, borrower),
1774 kPaymentInterval(LoanSet::kMinPaymentInterval),
1775 loanSetFee,
1776 Ter(tefBAD_AUTH));
1777 // sfPaymentInterval: too small (there is no max)
1778 env(set(evan, broker.brokerID, principalRequest),
1779 Sig(sfCounterpartySignature, lender),
1780 kPaymentInterval(LoanSet::kMinPaymentInterval - 1),
1781 loanSetFee,
1782 Ter(temINVALID));
1783
1784 // sfGracePeriod: good value, bad account
1785 env(set(evan, broker.brokerID, principalRequest),
1786 Sig(sfCounterpartySignature, borrower),
1787 kPaymentInterval(LoanSet::kMinPaymentInterval * 2),
1788 kGracePeriod(LoanSet::kMinPaymentInterval * 2),
1789 loanSetFee,
1790 Ter(tefBAD_AUTH));
1791 // sfGracePeriod: larger than paymentInterval
1792 env(set(evan, broker.brokerID, principalRequest),
1793 Sig(sfCounterpartySignature, lender),
1794 kPaymentInterval(LoanSet::kMinPaymentInterval * 2),
1795 kGracePeriod(LoanSet::kMinPaymentInterval * 3),
1796 loanSetFee,
1797 Ter(temINVALID));
1798
1799 // insufficient fee - single sign
1800 env(set(borrower, broker.brokerID, principalRequest),
1801 Sig(sfCounterpartySignature, lender),
1803 // insufficient fee - multisign
1804 env(signers(lender, 2, {{evan, 1}, {borrower, 1}}));
1805 env(signers(borrower, 2, {{evan, 1}, {lender, 1}}));
1806 env(set(borrower, broker.brokerID, principalRequest),
1807 kCounterparty(lender),
1808 Msig(evan, lender),
1809 Msig(sfCounterpartySignature, evan, borrower),
1810 Fee(env.current()->fees().base * 5 - 1),
1812 // Bad multisign signatures for borrower (Account)
1813 env(set(borrower, broker.brokerID, principalRequest),
1814 kCounterparty(lender),
1815 Msig(alice, issuer),
1816 Msig(sfCounterpartySignature, evan, borrower),
1817 Fee(env.current()->fees().base * 5),
1819 // Bad multisign signatures for issuer (Counterparty)
1820 env(set(borrower, broker.brokerID, principalRequest),
1821 kCounterparty(lender),
1822 Msig(evan, lender),
1823 Msig(sfCounterpartySignature, alice, issuer),
1824 Fee(env.current()->fees().base * 5 - 1),
1826 env(signers(lender, kNone));
1827 env(signers(borrower, kNone));
1828 // multisign sufficient fee, but no signers set up
1829 env(set(borrower, broker.brokerID, principalRequest),
1830 kCounterparty(lender),
1831 Msig(evan, lender),
1832 Msig(sfCounterpartySignature, evan, borrower),
1833 Fee(env.current()->fees().base * 5),
1835 // not the broker owner, no counterparty, not signed by broker
1836 // owner
1837 env(set(borrower, broker.brokerID, principalRequest),
1838 Sig(sfCounterpartySignature, evan),
1839 loanSetFee,
1840 Ter(tefBAD_AUTH));
1841 // not the broker owner, counterparty is borrower
1842 env(set(evan, broker.brokerID, principalRequest),
1843 kCounterparty(borrower),
1844 Sig(sfCounterpartySignature, borrower),
1845 loanSetFee,
1847 // not a LoanBroker object, no counterparty
1848 env(set(lender, badKeylet.key, principalRequest),
1849 Sig(sfCounterpartySignature, evan),
1850 loanSetFee,
1852 // not a LoanBroker object, counterparty is valid
1853 env(set(lender, badKeylet.key, principalRequest),
1854 kCounterparty(borrower),
1855 Sig(sfCounterpartySignature, borrower),
1856 loanSetFee,
1857 Ter(tecNO_ENTRY));
1858 // borrower doesn't exist
1859 env(set(lender, broker.brokerID, principalRequest),
1860 kCounterparty(alice),
1861 Sig(sfCounterpartySignature, alice),
1862 loanSetFee,
1864
1865 // Request more funds than the vault has available
1866 env(set(evan, broker.brokerID, totalVaultRequest + 1),
1867 Sig(sfCounterpartySignature, lender),
1868 loanSetFee,
1870
1871 // Request more funds than the broker's first-loss capital can
1872 // cover.
1873 env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1),
1874 Sig(sfCounterpartySignature, lender),
1875 loanSetFee,
1877
1878 // Frozen trust line / locked MPT issuance
1879 // XRP can not be frozen, but run through the loop anyway to test
1880 // the tecLIMIT_EXCEEDED case
1881 {
1882 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
1883 if (!BEAST_EXPECT(brokerSle))
1884 return;
1885
1886 auto const vaultPseudo = [&]() {
1887 auto const vaultSle = env.le(keylet::vault(brokerSle->at(sfVaultID)));
1888 if (!BEAST_EXPECT(vaultSle))
1889 {
1890 // This will be wrong, but the test has failed anyway.
1891 return Account{lender};
1892 }
1893 auto vaultPseudo = Account("Vault pseudo-account", vaultSle->at(sfAccount));
1894 return vaultPseudo;
1895 }();
1896
1897 auto const [freeze, deepfreeze, unfreeze, expectedResult] =
1898 [&]() -> std::tuple<
1899 std::function<void(Account const& holder)>,
1900 std::function<void(Account const& holder)>,
1901 std::function<void(Account const& holder)>,
1902 TER> {
1903 // Freeze / lock the asset
1904 std::function<void(Account const& holder)> const empty;
1905 if (broker.asset.native())
1906 {
1907 // XRP can't be frozen
1908 return std::make_tuple(empty, empty, empty, tesSUCCESS);
1909 }
1910 if (broker.asset.holds<Issue>())
1911 {
1912 auto freeze = [&](Account const& holder) {
1913 env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze));
1914 };
1915 auto deepfreeze = [&](Account const& holder) {
1916 env(trust(issuer, holder[iouCurrency_](0), tfSetFreeze | tfSetDeepFreeze));
1917 };
1918 auto unfreeze = [&](Account const& holder) {
1919 env(trust(
1920 issuer, holder[iouCurrency_](0), tfClearFreeze | tfClearDeepFreeze));
1921 };
1922 return std::make_tuple(freeze, deepfreeze, unfreeze, tecFROZEN);
1923 }
1924
1925 auto freeze = [&](Account const& holder) {
1926 mptt.set({.account = issuer, .holder = holder, .flags = tfMPTLock});
1927 };
1928 auto unfreeze = [&](Account const& holder) {
1929 mptt.set({.account = issuer, .holder = holder, .flags = tfMPTUnlock});
1930 };
1931 return std::make_tuple(freeze, empty, unfreeze, tecLOCKED);
1932 }();
1933
1934 // Try freezing the accounts that can't be frozen
1935 if (freeze)
1936 {
1937 for (auto const& account : {vaultPseudo, evan})
1938 {
1939 // Freeze the account
1940 freeze(account);
1941
1942 // Try to create a loan with a frozen line
1943 env(set(evan, broker.brokerID, debtMaximumRequest),
1944 Sig(sfCounterpartySignature, lender),
1945 loanSetFee,
1946 Ter(expectedResult));
1947
1948 // Unfreeze the account
1949 BEAST_EXPECT(unfreeze);
1950 unfreeze(account);
1951
1952 // Ensure the line is unfrozen with a request that is fine
1953 // except too it requests more principal than the broker can
1954 // carry
1955 env(set(evan, broker.brokerID, debtMaximumRequest + 1),
1956 Sig(sfCounterpartySignature, lender),
1957 loanSetFee,
1959 }
1960 }
1961
1962 // Deep freeze the borrower, which prevents them from receiving
1963 // funds
1964 if (deepfreeze)
1965 {
1966 // Make sure evan has a trust line that so the issuer can
1967 // freeze it. (Don't need to do this for the borrower,
1968 // because LoanSet will create a line to the borrower
1969 // automatically.)
1970 env(trust(evan, issuer[iouCurrency_](100'000)));
1971
1972 for (auto const& account : {// these accounts can't be frozen, which deep freeze
1973 // implies
1974 vaultPseudo,
1975 evan,
1976 // these accounts can't be deep frozen
1977 lender})
1978 {
1979 // Freeze evan
1980 deepfreeze(account);
1981
1982 // Try to create a loan with a deep frozen line
1983 env(set(evan, broker.brokerID, debtMaximumRequest),
1984 Sig(sfCounterpartySignature, lender),
1985 loanSetFee,
1986 Ter(expectedResult));
1987
1988 // Unfreeze evan
1989 BEAST_EXPECT(unfreeze);
1990 unfreeze(account);
1991
1992 // Ensure the line is unfrozen with a request that is fine
1993 // except too it requests more principal than the broker can
1994 // carry
1995 env(set(evan, broker.brokerID, debtMaximumRequest + 1),
1996 Sig(sfCounterpartySignature, lender),
1997 loanSetFee,
1999 }
2000 }
2001 }
2002
2003 // Finally! Create a loan
2004
2005 auto coverAvailable = [&env, this](uint256 const& brokerID, Number const& expected) {
2006 if (auto const brokerSle = env.le(keylet::loanBroker(brokerID));
2007 BEAST_EXPECT(brokerSle))
2008 {
2009 auto const available = brokerSle->at(sfCoverAvailable);
2010 BEAST_EXPECT(available == expected);
2011 return available;
2012 }
2013 return Number{};
2014 };
2015 auto getDefaultInfo = [&env, this](LoanState const& state, BrokerInfo const& broker) {
2016 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
2017 BEAST_EXPECT(brokerSle))
2018 {
2019 BEAST_EXPECT(
2020 state.loanScale >=
2021 (broker.asset.integral()
2022 ? 0
2023 : std::max(
2024 broker.vaultScale(env), state.principalOutstanding.exponent())));
2026 auto const defaultAmount = roundToAsset(
2027 broker.asset,
2028 std::min(
2031 brokerSle->at(sfDebtTotal), broker.params.coverRateMin),
2033 state.totalValue - state.managementFeeOutstanding),
2034 state.loanScale);
2035 return std::make_pair(defaultAmount, brokerSle->at(sfOwner));
2036 }
2037 return std::make_pair(Number{}, AccountID{});
2038 };
2039 auto replenishCover = [&env, &coverAvailable](
2040 BrokerInfo const& broker,
2041 AccountID const& brokerAcct,
2042 Number const& startingCoverAvailable,
2043 Number const& amountToBeCovered) {
2044 coverAvailable(broker.brokerID, startingCoverAvailable - amountToBeCovered);
2046 brokerAcct, broker.brokerID, STAmount{broker.asset, amountToBeCovered}));
2047 coverAvailable(broker.brokerID, startingCoverAvailable);
2048 env.close();
2049 };
2050
2051 auto defaultImmediately = [&](std::uint32_t baseFlag, bool impair = true) {
2052 return [&, impair, baseFlag](
2053 Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2054 // toEndOfLife
2055 //
2056 // Default the loan
2057
2058 // Initialize values with the current state
2059 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2060 BEAST_EXPECT(state.flags == baseFlag);
2061
2062 auto const& broker = verifyLoanStatus.broker;
2063 auto const startingCoverAvailable = coverAvailable(
2064 broker.brokerID, broker.asset(broker.params.coverDeposit).number());
2065
2066 if (impair)
2067 {
2068 // Check the vault
2069 bool const canImpair = canImpairLoan(env, broker, state);
2070 // Impair the loan, if possible
2071 env(manage(lender, loanKeylet.key, tfLoanImpair),
2072 canImpair ? Ter(tesSUCCESS) : Ter(tecLIMIT_EXCEEDED));
2073
2074 if (canImpair)
2075 {
2076 state.flags |= tfLoanImpair;
2077 state.nextPaymentDate = env.now().time_since_epoch().count();
2078
2079 // Once the loan is impaired, it can't be impaired again
2080 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2081 }
2082 verifyLoanStatus(state);
2083 }
2084
2085 auto const nextDueDate = tp{d{state.nextPaymentDate}};
2086
2087 // Can't default the loan yet. The grace period hasn't
2088 // expired
2089 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecTOO_SOON));
2090
2091 // Let some time pass so that the loan can be
2092 // defaulted
2093 env.close(nextDueDate + 60s);
2094
2095 auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker);
2096
2097 // Default the loan
2098 env(manage(lender, loanKeylet.key, tfLoanDefault));
2099 env.close();
2100
2101 // The LoanBroker just lost some of it's first-loss capital.
2102 // Replenish it.
2103 replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered);
2104
2105 state.flags |= tfLoanDefault;
2106 state.paymentRemaining = 0;
2107 state.totalValue = 0;
2108 state.principalOutstanding = 0;
2109 state.managementFeeOutstanding = 0;
2110 state.nextPaymentDate = 0;
2111 verifyLoanStatus(state);
2112
2113 // Once a loan is defaulted, it can't be managed
2114 env(manage(lender, loanKeylet.key, tfLoanUnimpair), Ter(tecNO_PERMISSION));
2115 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2116 // Can't make a payment on it either
2117 env(pay(borrower, loanKeylet.key, broker.asset(300)), Ter(tecKILLED));
2118 };
2119 };
2120
2121 auto singlePayment = [&](Keylet const& loanKeylet,
2122 VerifyLoanStatus const& verifyLoanStatus,
2123 LoanState& state,
2124 STAmount const& payoffAmount,
2125 std::uint32_t numPayments,
2126 std::uint32_t baseFlag,
2127 std::uint32_t txFlags) {
2128 // toEndOfLife
2129 //
2130 verifyLoanStatus(state);
2131
2132 // Send some bogus pay transactions
2133 env(pay(borrower, keylet::loan(uint256(0)).key, broker.asset(10), txFlags),
2134 Ter(temINVALID));
2135 // broker.asset(80) is less than a single payment, but all these
2136 // checks fail before that matters
2137 env(pay(borrower, loanKeylet.key, broker.asset(-80), txFlags), Ter(temBAD_AMOUNT));
2138 env(pay(borrower, broker.brokerID, broker.asset(80), txFlags), Ter(tecNO_ENTRY));
2139 env(pay(evan, loanKeylet.key, broker.asset(80), txFlags), Ter(tecNO_PERMISSION));
2140
2141 // TODO: Write a general "isFlag" function? See STObject::isFlag.
2142 // Maybe add a static overloaded member?
2143 if (!(state.flags & lsfLoanOverpayment))
2144 {
2145 // If the loan does not allow overpayments, send a payment that
2146 // tries to make an overpayment. Do not include `txFlags`, so we
2147 // don't end up duplicating the next test transaction.
2148 //
2149 // fixCleanup3_1_3 gates tfLoanOverpayment as a valid flag:
2150 // with fix on → preflight passes, apply returns tecNO_PERMISSION;
2151 // with fix off → preflight rejects the flag, returns temINVALID_FLAG.
2152 bool const hasFix313 = env.current()->rules().enabled(fixCleanup3_1_3);
2153 STAmount const overpayAmount{broker.asset, state.periodicPayment * Number{15, -1}};
2154 XRPAmount const overpayFee{
2155 baseFee * (Number{15, -1} / kLoanPaymentsPerFeeIncrement + 1)};
2156 env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment),
2157 Fee(overpayFee),
2158 Ter(hasFix313 ? TER{tecNO_PERMISSION} : TER{temINVALID_FLAG}));
2159
2160 if (hasFix313)
2161 {
2162 env.disableFeature(fixCleanup3_1_3);
2163 env(pay(borrower, loanKeylet.key, overpayAmount, tfLoanOverpayment),
2164 Fee(overpayFee),
2166 env.enableFeature(fixCleanup3_1_3);
2167 }
2168 }
2169 // Try to send a payment marked as multiple mutually exclusive
2170 // payment types. Do not include `txFlags`, so we don't duplicate
2171 // the prior test transaction.
2172 env(pay(borrower,
2173 loanKeylet.key,
2174 broker.asset(state.periodicPayment * 2),
2175 tfLoanLatePayment | tfLoanFullPayment),
2177 env(pay(borrower,
2178 loanKeylet.key,
2179 broker.asset(state.periodicPayment * 2),
2180 tfLoanLatePayment | tfLoanOverpayment),
2182 env(pay(borrower,
2183 loanKeylet.key,
2184 broker.asset(state.periodicPayment * 2),
2185 tfLoanOverpayment | tfLoanFullPayment),
2187 env(pay(borrower,
2188 loanKeylet.key,
2189 broker.asset(state.periodicPayment * 2),
2190 tfLoanLatePayment | tfLoanOverpayment | tfLoanFullPayment),
2192
2193 {
2194 auto const otherAsset =
2195 broker.asset.raw() == assets[0].raw() ? assets[1] : assets[0];
2196 env(pay(borrower, loanKeylet.key, otherAsset(100), txFlags), Ter(tecWRONG_ASSET));
2197 }
2198
2199 // Amount doesn't cover a single payment
2200 env(pay(borrower, loanKeylet.key, STAmount{broker.asset, 1}, txFlags),
2202
2203 // Get the balance after these failed transactions take
2204 // fees
2205 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
2206
2207 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2208 // Try to pay a little extra to show that it's _not_
2209 // taken
2210 auto const transactionAmount = payoffAmount + broker.asset(10);
2211
2212 // Send a transaction that tries to pay more than the borrowers's
2213 // balance
2214 XRPAmount const badFee{
2215 baseFee *
2216 (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment /
2217 kLoanPaymentsPerFeeIncrement +
2218 1)};
2219 env(pay(borrower,
2220 loanKeylet.key,
2221 STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2},
2222 txFlags),
2223 Fee(badFee),
2225
2226 XRPAmount const goodFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)};
2227 env(pay(borrower, loanKeylet.key, transactionAmount, txFlags), Fee(goodFee));
2228
2229 env.close();
2230
2231 // log << env.meta()->getJson() << std::endl;
2232
2233 // Need to account for fees if the loan is in XRP
2234 PrettyAmount adjustment = broker.asset(0);
2235 if (broker.asset.native())
2236 {
2237 adjustment = badFee + goodFee;
2238 }
2239
2240 state.paymentRemaining = 0;
2241 state.principalOutstanding = 0;
2242 state.totalValue = 0;
2243 state.managementFeeOutstanding = 0;
2244 state.previousPaymentDate =
2245 state.nextPaymentDate + (state.paymentInterval * (numPayments - 1));
2246 state.nextPaymentDate = 0;
2247 verifyLoanStatus(state);
2248
2249 verifyLoanStatus.checkPayment(
2250 state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment);
2251
2252 // Can't impair or default a paid off loan
2253 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2254 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION));
2255 };
2256
2257 auto fullPayment = [&](std::uint32_t baseFlag) {
2258 return [&, baseFlag](
2259 Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2260 // toEndOfLife
2261 //
2262 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2263 env.close(state.startDate + 20s);
2264 auto const loanAge = (env.now() - state.startDate).count();
2265 BEAST_EXPECT(loanAge == 30);
2266
2267 // Full payoff amount will consist of
2268 // 1. principal outstanding (1000)
2269 // 2. accrued interest (at 12%)
2270 // 3. prepayment penalty (closeInterest at 3.6%)
2271 // 4. close payment fee (4)
2272 // Calculate these values without the helper functions
2273 // to verify they're working correctly The numbers in
2274 // the below BEAST_EXPECTs may not hold across assets.
2275 Number const interval = state.paymentInterval;
2276 auto const periodicRate = interval * Number(12, -2) / kSecondsInYear;
2277 BEAST_EXPECT(
2278 periodicRate == Number(2283105022831050228ULL, -24, Number::Normalized{}));
2279 STAmount const principalOutstanding{broker.asset, state.principalOutstanding};
2280 STAmount const accruedInterest{
2281 broker.asset, state.principalOutstanding * periodicRate * loanAge / interval};
2282 BEAST_EXPECT(accruedInterest == broker.asset(Number(1141552511415525, -19)));
2283 STAmount const prepaymentPenalty{
2284 broker.asset, state.principalOutstanding * Number(36, -3)};
2285 BEAST_EXPECT(prepaymentPenalty == broker.asset(36));
2286 STAmount const closePaymentFee = broker.asset(4);
2287 auto const payoffAmount = roundToScale(
2288 principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee,
2289 state.loanScale);
2290 BEAST_EXPECT(
2291 payoffAmount ==
2293 broker.asset,
2294 broker.asset(Number(1040000114155251, -12)).number(),
2295 state.loanScale));
2296
2297 // The terms of this loan actually make the early payoff
2298 // more expensive than just making payments
2299 BEAST_EXPECT(
2300 payoffAmount >
2301 state.paymentRemaining * (state.periodicPayment + broker.asset(2).value()));
2302
2303 singlePayment(
2304 loanKeylet,
2305 verifyLoanStatus,
2306 state,
2307 payoffAmount,
2308 1,
2309 baseFlag,
2310 tfLoanFullPayment);
2311 };
2312 };
2313
2314 auto combineAllPayments = [&](std::uint32_t baseFlag) {
2315 return
2316 [&, baseFlag](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2317 // toEndOfLife
2318 //
2319
2320 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2321 env.close();
2322
2323 BEAST_EXPECT(
2324 STAmount(broker.asset, state.periodicPayment) ==
2325 broker.asset(Number(8333457002039338267, -17)));
2326
2327 // Make all the payments in one transaction
2328 // service fee is 2
2329 auto const startingPayments = state.paymentRemaining;
2330 STAmount const payoffAmount = [&]() {
2332 auto const rawPayoff =
2333 startingPayments * (state.periodicPayment + broker.asset(2).value());
2334 STAmount payoffAmount{broker.asset, rawPayoff};
2335 BEAST_EXPECTS(
2336 payoffAmount == broker.asset(Number(1024014840244721, -12)),
2337 to_string(payoffAmount));
2338 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2339
2340 payoffAmount = roundToScale(payoffAmount, state.loanScale);
2341
2342 return payoffAmount;
2343 }();
2344
2345 auto const totalPayoffValue =
2346 state.totalValue + startingPayments * broker.asset(2).value();
2347 STAmount const totalPayoffAmount{broker.asset, totalPayoffValue};
2348
2349 BEAST_EXPECTS(
2350 totalPayoffAmount == payoffAmount,
2351 "Payoff amount: " + to_string(payoffAmount) +
2352 ". Total Value: " + to_string(totalPayoffAmount));
2353
2354 singlePayment(
2355 loanKeylet,
2356 verifyLoanStatus,
2357 state,
2358 payoffAmount,
2359 state.paymentRemaining,
2360 baseFlag,
2361 0);
2362 };
2363 };
2364
2365 // There are a lot of fields that can be set on a loan, but most
2366 // of them only affect the "math" when a payment is made. The
2367 // only one that really affects behavior is the
2368 // `tfLoanOverpayment` flag.
2369 lifecycle(
2370 caseLabel,
2371 "Loan overpayment allowed - Impair and Default",
2372 env,
2373 loanAmount,
2374 interestExponent,
2375 lender,
2376 borrower,
2377 evan,
2378 broker,
2379 pseudoAcct,
2380 tfLoanOverpayment,
2381 defaultImmediately(lsfLoanOverpayment));
2382
2383 lifecycle(
2384 caseLabel,
2385 "Loan overpayment prohibited - Impair and Default",
2386 env,
2387 loanAmount,
2388 interestExponent,
2389 lender,
2390 borrower,
2391 evan,
2392 broker,
2393 pseudoAcct,
2394 0,
2395 defaultImmediately(0));
2396
2397 lifecycle(
2398 caseLabel,
2399 "Loan overpayment allowed - Default without Impair",
2400 env,
2401 loanAmount,
2402 interestExponent,
2403 lender,
2404 borrower,
2405 evan,
2406 broker,
2407 pseudoAcct,
2408 tfLoanOverpayment,
2409 defaultImmediately(lsfLoanOverpayment, false));
2410
2411 lifecycle(
2412 caseLabel,
2413 "Loan overpayment prohibited - Default without Impair",
2414 env,
2415 loanAmount,
2416 interestExponent,
2417 lender,
2418 borrower,
2419 evan,
2420 broker,
2421 pseudoAcct,
2422 0,
2423 defaultImmediately(0, false));
2424
2425 lifecycle(
2426 caseLabel,
2427 "Loan overpayment prohibited - Pay off immediately",
2428 env,
2429 loanAmount,
2430 interestExponent,
2431 lender,
2432 borrower,
2433 evan,
2434 broker,
2435 pseudoAcct,
2436 0,
2437 fullPayment(0));
2438
2439 lifecycle(
2440 caseLabel,
2441 "Loan overpayment allowed - Pay off immediately",
2442 env,
2443 loanAmount,
2444 interestExponent,
2445 lender,
2446 borrower,
2447 evan,
2448 broker,
2449 pseudoAcct,
2450 tfLoanOverpayment,
2451 fullPayment(lsfLoanOverpayment));
2452
2453 lifecycle(
2454 caseLabel,
2455 "Loan overpayment prohibited - Combine all payments",
2456 env,
2457 loanAmount,
2458 interestExponent,
2459 lender,
2460 borrower,
2461 evan,
2462 broker,
2463 pseudoAcct,
2464 0,
2465 combineAllPayments(0));
2466
2467 lifecycle(
2468 caseLabel,
2469 "Loan overpayment allowed - Combine all payments",
2470 env,
2471 loanAmount,
2472 interestExponent,
2473 lender,
2474 borrower,
2475 evan,
2476 broker,
2477 pseudoAcct,
2478 tfLoanOverpayment,
2479 combineAllPayments(lsfLoanOverpayment));
2480
2481 lifecycle(
2482 caseLabel,
2483 "Loan overpayment prohibited - Make payments",
2484 env,
2485 loanAmount,
2486 interestExponent,
2487 lender,
2488 borrower,
2489 evan,
2490 broker,
2491 pseudoAcct,
2492 0,
2493 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2494 // toEndOfLife
2495 //
2496 // Draw and make multiple payments
2497 auto state = getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2498 BEAST_EXPECT(state.flags == 0);
2499 env.close();
2500
2501 verifyLoanStatus(state);
2502
2503 env.close(state.startDate + 20s);
2504 auto const loanAge = (env.now() - state.startDate).count();
2505 BEAST_EXPECT(loanAge == 30);
2506
2507 // Periodic payment amount will consist of
2508 // 1. principal outstanding (1000)
2509 // 2. interest interest rate (at 12%)
2510 // 3. payment interval (600s)
2511 // 4. loan service fee (2)
2512 // Calculate these values without the helper functions
2513 // to verify they're working correctly The numbers in
2514 // the below BEAST_EXPECTs may not hold across assets.
2515 Number const interval = state.paymentInterval;
2516 auto const periodicRate = interval * Number(12, -2) / kSecondsInYear;
2517 BEAST_EXPECT(
2518 periodicRate == Number(2283105022831050228, -24, Number::Normalized{}));
2519 STAmount const roundedPeriodicPayment{
2520 broker.asset,
2521 roundPeriodicPayment(broker.asset, state.periodicPayment, state.loanScale)};
2522
2523 testcase << currencyLabel << " Payment components: "
2524 << "Payments remaining, rawInterest, rawPrincipal, "
2525 "rawMFee, trackedValueDelta, trackedPrincipalDelta, "
2526 "trackedInterestDelta, trackedMgmtFeeDelta, special";
2527
2528 auto const serviceFee = broker.asset(2);
2529
2530 BEAST_EXPECT(
2531 roundedPeriodicPayment ==
2533 broker.asset(
2534 Number(8333457002039338267, -17), Number::RoundingMode::Upward),
2535 state.loanScale,
2537 // 83334570.01162141
2538 // Include the service fee
2539 STAmount const totalDue = roundToScale(
2540 roundedPeriodicPayment + serviceFee,
2541 state.loanScale,
2543 // Only check the first payment since the rounding
2544 // may drift as payments are made
2545 BEAST_EXPECT(
2546 totalDue ==
2548 broker.asset(
2549 Number(8533457002039338267, -17), Number::RoundingMode::Upward),
2550 state.loanScale,
2552
2553 {
2554 auto const raw = computeTheoreticalLoanState(
2555 env.current()->rules(),
2556 state.periodicPayment,
2557 periodicRate,
2558 state.paymentRemaining,
2559 broker.params.managementFeeRate);
2560 auto const rounded = constructLoanState(
2561 state.totalValue,
2562 state.principalOutstanding,
2563 state.managementFeeOutstanding);
2564 testcase << currencyLabel << " Loan starting state: " << state.paymentRemaining
2565 << ", " << raw.interestDue << ", " << raw.principalOutstanding << ", "
2566 << raw.managementFeeDue << ", " << rounded.valueOutstanding << ", "
2567 << rounded.principalOutstanding << ", " << rounded.interestDue << ", "
2568 << rounded.managementFeeDue;
2569 }
2570
2571 // Try to pay a little extra to show that it's _not_
2572 // taken
2573 STAmount const transactionAmount =
2574 STAmount{broker.asset, totalDue} + broker.asset(10);
2575 // Only check the first payment since the rounding
2576 // may drift as payments are made
2577 BEAST_EXPECT(
2578 transactionAmount ==
2580 broker.asset(Number(9533457002039400, -14), Number::RoundingMode::Upward),
2581 state.loanScale,
2583
2584 auto const initialState = state;
2586 .trackedValueDelta = 0,
2587 .trackedPrincipalDelta = 0,
2588 .trackedManagementFeeDelta = 0};
2589 Number totalInterestPaid = 0;
2590 std::size_t totalPaymentsMade = 0;
2591
2593 env.current()->rules(),
2594 state.periodicPayment,
2595 periodicRate,
2596 state.paymentRemaining,
2597 broker.params.managementFeeRate);
2598
2599 while (state.paymentRemaining > 0)
2600 {
2601 // Compute the expected principal amount
2602 auto const paymentComponents = xrpl::detail::computePaymentComponents(
2603 env.current()->rules(),
2604 broker.asset.raw(),
2605 state.loanScale,
2606 state.totalValue,
2607 state.principalOutstanding,
2608 state.managementFeeOutstanding,
2609 state.periodicPayment,
2610 periodicRate,
2611 state.paymentRemaining,
2612 broker.params.managementFeeRate);
2613
2614 BEAST_EXPECTS(
2615 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2616 paymentComponents.trackedValueDelta <= roundedPeriodicPayment,
2617 "Delta: " + to_string(paymentComponents.trackedValueDelta) +
2618 ", periodic payment: " + to_string(roundedPeriodicPayment));
2619
2620 xrpl::LoanState const nextTrueState = computeTheoreticalLoanState(
2621 env.current()->rules(),
2622 state.periodicPayment,
2623 periodicRate,
2624 state.paymentRemaining - 1,
2625 broker.params.managementFeeRate);
2626 xrpl::detail::LoanStateDeltas const deltas = currentTrueState - nextTrueState;
2627
2628 testcase << currencyLabel << " Payment components: " << state.paymentRemaining
2629 << ", " << deltas.interest << ", " << deltas.principal << ", "
2630 << deltas.managementFee << ", " << paymentComponents.trackedValueDelta
2631 << ", " << paymentComponents.trackedPrincipalDelta << ", "
2632 << paymentComponents.trackedInterestPart() << ", "
2633 << paymentComponents.trackedManagementFeeDelta << ", "
2634 << [&]() -> char const* {
2635 if (paymentComponents.specialCase ==
2637 return "final";
2638 if (paymentComponents.specialCase ==
2640 return "extra";
2641 return "none";
2642 }();
2643
2644 auto const totalDueAmount = STAmount{
2645 broker.asset, paymentComponents.trackedValueDelta + serviceFee.number()};
2646
2647 // Due to the rounding algorithms to keep the interest and
2648 // principal in sync with "true" values, the computed amount
2649 // may be a little less than the rounded fixed payment
2650 // amount. For integral types, the difference should be < 3
2651 // (1 unit for each of the interest and management fee). For
2652 // IOUs, the difference should be after the 8th digit.
2653 Number const diff = totalDue - totalDueAmount;
2654 BEAST_EXPECT(
2655 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2656 diff == beast::kZero ||
2657 (diff > beast::kZero &&
2658 ((broker.asset.integral() && (static_cast<Number>(diff) < 3)) ||
2659 (state.loanScale - diff.exponent() > 13))));
2660
2661 BEAST_EXPECT(
2662 paymentComponents.trackedValueDelta ==
2663 paymentComponents.trackedPrincipalDelta +
2664 paymentComponents.trackedInterestPart() +
2665 paymentComponents.trackedManagementFeeDelta);
2666 BEAST_EXPECT(
2667 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2668 paymentComponents.trackedValueDelta <= roundedPeriodicPayment);
2669
2670 BEAST_EXPECT(
2671 state.paymentRemaining < 12 ||
2673 broker.asset,
2674 deltas.principal,
2675 state.loanScale,
2678 broker.asset(
2679 Number(8333228691531218890, -17), Number::RoundingMode::Upward),
2680 state.loanScale,
2682 BEAST_EXPECT(
2683 paymentComponents.trackedPrincipalDelta >= beast::kZero &&
2684 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
2685 BEAST_EXPECT(
2686 paymentComponents.specialCase != xrpl::detail::PaymentSpecialCase::Final ||
2687 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
2688 BEAST_EXPECT(
2689 paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final ||
2690 (state.periodicPayment.exponent() -
2691 (deltas.principal + deltas.interest + deltas.managementFee -
2692 state.periodicPayment)
2693 .exponent()) > 14);
2694
2695 auto const borrowerBalanceBeforePayment = env.balance(borrower, broker.asset);
2696
2697 if (canImpairLoan(env, broker, state))
2698 {
2699 // Making a payment will unimpair the loan
2700 env(manage(lender, loanKeylet.key, tfLoanImpair));
2701 }
2702
2703 env.close();
2704
2705 // Make the payment
2706 env(pay(borrower, loanKeylet.key, transactionAmount));
2707
2708 env.close();
2709
2710 // Need to account for fees if the loan is in XRP
2711 PrettyAmount adjustment = broker.asset(0);
2712 if (broker.asset.native())
2713 {
2714 adjustment = env.current()->fees().base;
2715 }
2716
2717 // Check the result
2718 verifyLoanStatus.checkPayment(
2719 state.loanScale,
2720 borrower,
2721 borrowerBalanceBeforePayment,
2722 totalDueAmount,
2723 adjustment);
2724
2725 --state.paymentRemaining;
2726 state.previousPaymentDate = state.nextPaymentDate;
2727 if (paymentComponents.specialCase == xrpl::detail::PaymentSpecialCase::Final)
2728 {
2729 state.paymentRemaining = 0;
2730 state.nextPaymentDate = 0;
2731 }
2732 else
2733 {
2734 state.nextPaymentDate += state.paymentInterval;
2735 }
2736 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
2737 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
2738 state.totalValue -= paymentComponents.trackedValueDelta;
2739
2740 verifyLoanStatus(state);
2741
2742 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
2743 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
2744 totalPaid.trackedManagementFeeDelta +=
2745 paymentComponents.trackedManagementFeeDelta;
2746 totalInterestPaid += paymentComponents.trackedInterestPart();
2747 ++totalPaymentsMade;
2748
2749 currentTrueState = nextTrueState;
2750 }
2751
2752 // Loan is paid off
2753 BEAST_EXPECT(state.paymentRemaining == 0);
2754 BEAST_EXPECT(state.principalOutstanding == 0);
2755
2756 // Make sure all the payments add up
2757 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
2758 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
2759 BEAST_EXPECT(
2760 totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
2761 // This is almost a tautology given the previous checks, but
2762 // check it anyway for completeness.
2763 BEAST_EXPECT(
2764 totalInterestPaid ==
2765 initialState.totalValue -
2766 (initialState.principalOutstanding +
2767 initialState.managementFeeOutstanding));
2768 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
2769
2770 // Can't impair or default a paid off loan
2771 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tecNO_PERMISSION));
2772 env(manage(lender, loanKeylet.key, tfLoanDefault), Ter(tecNO_PERMISSION));
2773 });
2774
2775#if LOAN_TODO
2776 // TODO
2777
2778 /*
2779 LoanPay fails with tecINVARIANT_FAILED error when loan_broker(also
2780 borrower) tries to do the payment. Here's the scenario: Create a XRP
2781 loan with loan broker as borrower, loan origination fee and loan service
2782 fee. Loan broker makes the first payment with periodic payment and loan
2783 service fee.
2784 */
2785
2786 auto time = [&](std::string label, std::function<void()> timed) {
2787 if (!BEAST_EXPECT(timed))
2788 return;
2789
2790 using clock_type = std::chrono::steady_clock;
2791 using duration_type = std::chrono::milliseconds;
2792
2793 auto const start = clock_type::now();
2794 timed();
2795 auto const duration =
2796 std::chrono::duration_cast<duration_type>(clock_type::now() - start);
2797
2798 log << label << " took " << duration.count() << "ms" << std::endl;
2799
2800 return duration;
2801 };
2802
2803 lifecycle(
2804 caseLabel,
2805 "timing",
2806 env,
2807 loanAmount,
2808 interestExponent,
2809 lender,
2810 borrower,
2811 evan,
2812 broker,
2813 pseudoAcct,
2814 tfLoanOverpayment,
2815 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) {
2816 // Estimate optimal values for kLoanPaymentsPerFeeIncrement and
2817 // kLoanMaximumPaymentsPerTransaction.
2818 using namespace loan;
2819
2820 auto const state = getCurrentState(env, broker, verifyLoanStatus.keylet);
2821 auto const serviceFee = broker.asset(2).value();
2822
2823 STAmount const totalDue{
2824 broker.asset,
2826 broker.asset, state.periodicPayment + serviceFee, state.loanScale)};
2827
2828 // Make a single payment
2829 time("single payment", [&]() { env(pay(borrower, loanKeylet.key, totalDue)); });
2830 env.close();
2831
2832 // Make all but the final payment
2833 auto const numPayments = (state.paymentRemaining - 2);
2834 STAmount const bigPayment{broker.asset, totalDue * numPayments};
2835 XRPAmount const bigFee{baseFee * (numPayments / kLoanPaymentsPerFeeIncrement + 1)};
2836 time("ten payments", [&]() {
2837 env(pay(borrower, loanKeylet.key, bigPayment), Fee(bigFee));
2838 });
2839 env.close();
2840
2841 time("final payment", [&]() {
2842 // Make the final payment
2843 env(pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1}));
2844 });
2845 env.close();
2846 });
2847
2848 lifecycle(
2849 caseLabel,
2850 "Loan overpayment allowed - Explicit overpayment",
2851 env,
2852 loanAmount,
2853 interestExponent,
2854 lender,
2855 borrower,
2856 evan,
2857 broker,
2858 pseudoAcct,
2859 tfLoanOverpayment,
2860 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2861
2862 lifecycle(
2863 caseLabel,
2864 "Loan overpayment prohibited - Late payment",
2865 env,
2866 loanAmount,
2867 interestExponent,
2868 lender,
2869 borrower,
2870 evan,
2871 broker,
2872 pseudoAcct,
2873 tfLoanOverpayment,
2874 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2875
2876 lifecycle(
2877 caseLabel,
2878 "Loan overpayment allowed - Late payment",
2879 env,
2880 loanAmount,
2881 interestExponent,
2882 lender,
2883 borrower,
2884 evan,
2885 broker,
2886 pseudoAcct,
2887 tfLoanOverpayment,
2888 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2889
2890 lifecycle(
2891 caseLabel,
2892 "Loan overpayment allowed - Late payment and overpayment",
2893 env,
2894 loanAmount,
2895 interestExponent,
2896 lender,
2897 borrower,
2898 evan,
2899 broker,
2900 pseudoAcct,
2901 tfLoanOverpayment,
2902 [&](Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { throw 0; });
2903
2904#endif
2905 }
2906
2907 void
2909 {
2910 using namespace jtx;
2911
2912 Account const issuer{"issuer"};
2913 Account const lender{"lender"};
2914 Account const borrower{"borrower"};
2915
2916 struct CaseArgs
2917 {
2918 bool requireAuth = false;
2919 bool authorizeBorrower = false;
2920 int initialXRP = 1'000'000;
2921 };
2922
2923 auto const testCase = [&, this](
2924 std::function<void(Env&, BrokerInfo const&, MPTTester&)> mptTest,
2925 std::function<void(Env&, BrokerInfo const&)> iouTest,
2926 CaseArgs args = {}) {
2927 Env env(*this, features);
2928 env.fund(XRP(args.initialXRP), issuer, lender, borrower);
2929 env.close();
2930 if (args.requireAuth)
2931 {
2932 env(fset(issuer, asfRequireAuth));
2933 env.close();
2934 }
2935
2936 // We need two different asset types, MPT and IOU. Prepare MPT
2937 // first
2938 MPTTester mptt{env, issuer, kMptInitNoFund};
2939
2940 auto const kNone = LedgerSpecificFlags(0);
2941 mptt.create(
2942 {.flags = tfMPTCanTransfer | tfMPTCanLock |
2943 (args.requireAuth ? tfMPTRequireAuth : kNone)});
2944 env.close();
2945 PrettyAsset const mptAsset = mptt.issuanceID();
2946 mptt.authorize({.account = lender});
2947 mptt.authorize({.account = borrower});
2948 env.close();
2949 if (args.requireAuth)
2950 {
2951 mptt.authorize({.account = issuer, .holder = lender});
2952 if (args.authorizeBorrower)
2953 mptt.authorize({.account = issuer, .holder = borrower});
2954 env.close();
2955 }
2956
2957 env(pay(issuer, lender, mptAsset(10'000'000)));
2958 env.close();
2959
2960 // Prepare IOU
2961 PrettyAsset const iouAsset = issuer[iouCurrency_];
2962 env(trust(lender, iouAsset(10'000'000)));
2963 env(trust(borrower, iouAsset(10'000'000)));
2964 env.close();
2965 if (args.requireAuth)
2966 {
2967 env(trust(issuer, iouAsset(0), lender, tfSetfAuth));
2968 env(pay(issuer, lender, iouAsset(10'000'000)));
2969 if (args.authorizeBorrower)
2970 {
2971 env(trust(issuer, iouAsset(0), borrower, tfSetfAuth));
2972 env(pay(issuer, borrower, iouAsset(10'000)));
2973 }
2974 }
2975 else
2976 {
2977 env(pay(issuer, lender, iouAsset(10'000'000)));
2978 env(pay(issuer, borrower, iouAsset(10'000)));
2979 }
2980 env.close();
2981
2982 // Create vaults and loan brokers
2983 std::array const assets{mptAsset, iouAsset};
2985 brokers.reserve(assets.size());
2986 for (auto const& asset : assets)
2987 {
2988 brokers.emplace_back(createVaultAndBroker(env, asset, lender));
2989 }
2990
2991 if (mptTest)
2992 mptTest(env, brokers[0], mptt);
2993 if (iouTest)
2994 iouTest(env, brokers[1]);
2995 };
2996
2997 testCase(
2998 [&, this](Env& env, BrokerInfo const& broker, auto&) {
2999 using namespace loan;
3000 Number const principalRequest = broker.asset(1'000).value();
3001
3002 testcase("MPT issuer is borrower, issuer submits");
3003 env(set(issuer, broker.brokerID, principalRequest),
3004 kCounterparty(lender),
3005 Sig(sfCounterpartySignature, lender),
3006 Fee(env.current()->fees().base * 5));
3007
3008 testcase("MPT issuer is borrower, lender submits");
3009 env(set(lender, broker.brokerID, principalRequest),
3010 kCounterparty(issuer),
3011 Sig(sfCounterpartySignature, issuer),
3012 Fee(env.current()->fees().base * 5));
3013 },
3014 [&, this](Env& env, BrokerInfo const& broker) {
3015 using namespace loan;
3016 Number const principalRequest = broker.asset(1'000).value();
3017
3018 testcase("IOU issuer is borrower, issuer submits");
3019 env(set(issuer, broker.brokerID, principalRequest),
3020 kCounterparty(lender),
3021 Sig(sfCounterpartySignature, lender),
3022 Fee(env.current()->fees().base * 5));
3023
3024 testcase("IOU issuer is borrower, lender submits");
3025 env(set(lender, broker.brokerID, principalRequest),
3026 kCounterparty(issuer),
3027 Sig(sfCounterpartySignature, issuer),
3028 Fee(env.current()->fees().base * 5));
3029 },
3030 CaseArgs{.requireAuth = true});
3031
3032 testCase(
3033 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3034 using namespace loan;
3035 Number const principalRequest = broker.asset(1'000).value();
3036
3037 testcase("MPT unauthorized borrower, borrower submits");
3038 env(set(borrower, broker.brokerID, principalRequest),
3039 kCounterparty(lender),
3040 Sig(sfCounterpartySignature, lender),
3041 Fee(env.current()->fees().base * 5),
3042 Ter{tecNO_AUTH});
3043
3044 testcase("MPT unauthorized borrower, lender submits");
3045 env(set(lender, broker.brokerID, principalRequest),
3046 kCounterparty(borrower),
3047 Sig(sfCounterpartySignature, borrower),
3048 Fee(env.current()->fees().base * 5),
3049 Ter{tecNO_AUTH});
3050 },
3051 [&, this](Env& env, BrokerInfo const& broker) {
3052 using namespace loan;
3053 Number const principalRequest = broker.asset(1'000).value();
3054
3055 testcase("IOU unauthorized borrower, borrower submits");
3056 env(set(borrower, broker.brokerID, principalRequest),
3057 kCounterparty(lender),
3058 Sig(sfCounterpartySignature, lender),
3059 Fee(env.current()->fees().base * 5),
3060 Ter{tecNO_AUTH});
3061
3062 testcase("IOU unauthorized borrower, lender submits");
3063 env(set(lender, broker.brokerID, principalRequest),
3064 kCounterparty(borrower),
3065 Sig(sfCounterpartySignature, borrower),
3066 Fee(env.current()->fees().base * 5),
3067 Ter{tecNO_AUTH});
3068 },
3069 CaseArgs{.requireAuth = true});
3070
3071 auto const [acctReserve, incReserve] = [this]() -> std::pair<int, int> {
3072 Env const env{*this, testableAmendments()};
3073 return {
3074 env.current()->fees().accountReserve(0).drops() / kDropsPerXrp.drops(),
3075 env.current()->fees().increment.drops() / kDropsPerXrp.drops()};
3076 }();
3077
3078 testCase(
3079 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
3080 using namespace loan;
3081 Number const principalRequest = broker.asset(1'000).value();
3082
3083 testcase(
3084 "MPT authorized borrower, borrower submits, borrower has "
3085 "no reserve");
3086 mptt.authorize({.account = borrower, .flags = tfMPTUnauthorize});
3087 env.close();
3088
3089 auto const mptoken = keylet::mptoken(mptt.issuanceID(), borrower);
3090 auto const sleMPT1 = env.le(mptoken);
3091 BEAST_EXPECT(sleMPT1 == nullptr);
3092
3093 // Burn some XRP
3094 env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2))));
3095 env.close();
3096
3097 // Cannot create loan, not enough reserve to create MPToken
3098 env(set(borrower, broker.brokerID, principalRequest),
3099 kCounterparty(lender),
3100 Sig(sfCounterpartySignature, lender),
3101 Fee(env.current()->fees().base * 5),
3102 Ter{tecINSUFFICIENT_RESERVE});
3103 env.close();
3104
3105 // Can create loan now, will implicitly create MPToken
3106 env(pay(issuer, borrower, XRP(incReserve)));
3107 env.close();
3108 env(set(borrower, broker.brokerID, principalRequest),
3109 kCounterparty(lender),
3110 Sig(sfCounterpartySignature, lender),
3111 Fee(env.current()->fees().base * 5));
3112 env.close();
3113
3114 auto const sleMPT2 = env.le(mptoken);
3115 BEAST_EXPECT(sleMPT2 != nullptr);
3116 },
3117 {},
3118 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3119
3120 testCase(
3121 {},
3122 [&, this](Env& env, BrokerInfo const& broker) {
3123 using namespace loan;
3124 Number const principalRequest = broker.asset(1'000).value();
3125
3126 testcase(
3127 "IOU authorized borrower, borrower submits, borrower has "
3128 "no reserve");
3129 // Remove trust line from borrower to issuer
3130 env.trust(broker.asset(0), borrower);
3131 env.close();
3132
3133 env(pay(borrower, issuer, broker.asset(10'000)));
3134 env.close();
3135 auto const trustline = keylet::trustLine(borrower, broker.asset.raw().get<Issue>());
3136 auto const sleLine1 = env.le(trustline);
3137 BEAST_EXPECT(sleLine1 == nullptr);
3138
3139 // Burn some XRP
3140 env(noop(borrower), Fee(XRP((acctReserve * 2) + (incReserve * 2))));
3141 env.close();
3142
3143 // Cannot create loan, not enough reserve to create trust line
3144 env(set(borrower, broker.brokerID, principalRequest),
3145 kCounterparty(lender),
3146 Sig(sfCounterpartySignature, lender),
3147 Fee(env.current()->fees().base * 5),
3148 Ter{tecNO_LINE_INSUF_RESERVE});
3149 env.close();
3150
3151 // Can create loan now, will implicitly create trust line
3152 env(pay(issuer, borrower, XRP(incReserve)));
3153 env.close();
3154 env(set(borrower, broker.brokerID, principalRequest),
3155 kCounterparty(lender),
3156 Sig(sfCounterpartySignature, lender),
3157 Fee(env.current()->fees().base * 5));
3158 env.close();
3159
3160 auto const sleLine2 = env.le(trustline);
3161 BEAST_EXPECT(sleLine2 != nullptr);
3162 },
3163 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3164
3165 testCase(
3166 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
3167 using namespace loan;
3168 Number const principalRequest = broker.asset(1'000).value();
3169
3170 testcase(
3171 "MPT authorized borrower, borrower submits, lender has "
3172 "no reserve");
3173 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
3174 auto const sleMPT1 = env.le(mptoken);
3175 BEAST_EXPECT(sleMPT1 != nullptr);
3176
3177 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
3178 env.close();
3179
3180 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
3181 env.close();
3182
3183 auto const sleMPT2 = env.le(mptoken);
3184 BEAST_EXPECT(sleMPT2 == nullptr);
3185
3186 // Burn some XRP
3187 env(noop(lender), Fee(XRP(incReserve)));
3188 env.close();
3189
3190 // Cannot create loan, not enough reserve to create MPToken
3191 env(set(borrower, broker.brokerID, principalRequest),
3192 kLoanOriginationFee(broker.asset(1).value()),
3193 kCounterparty(lender),
3194 Sig(sfCounterpartySignature, lender),
3195 Fee(env.current()->fees().base * 5),
3196 Ter{tecINSUFFICIENT_RESERVE});
3197 env.close();
3198
3199 // Can create loan now, will implicitly create MPToken
3200 env(pay(issuer, lender, XRP(incReserve)));
3201 env.close();
3202 env(set(borrower, broker.brokerID, principalRequest),
3203 kLoanOriginationFee(broker.asset(1).value()),
3204 kCounterparty(lender),
3205 Sig(sfCounterpartySignature, lender),
3206 Fee(env.current()->fees().base * 5));
3207 env.close();
3208
3209 auto const sleMPT3 = env.le(mptoken);
3210 BEAST_EXPECT(sleMPT3 != nullptr);
3211 },
3212 {},
3213 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3214
3215 testCase(
3216 {},
3217 [&, this](Env& env, BrokerInfo const& broker) {
3218 using namespace loan;
3219 Number const principalRequest = broker.asset(1'000).value();
3220
3221 testcase(
3222 "IOU authorized borrower, borrower submits, lender has no "
3223 "reserve");
3224 // Remove trust line from lender to issuer
3225 env.trust(broker.asset(0), lender);
3226 env.close();
3227
3228 auto const trustline = keylet::trustLine(lender, broker.asset.raw().get<Issue>());
3229 auto const sleLine1 = env.le(trustline);
3230 BEAST_EXPECT(sleLine1 != nullptr);
3231
3232 env(pay(lender, issuer, broker.asset(abs(sleLine1->at(sfBalance).value()))));
3233 env.close();
3234 auto const sleLine2 = env.le(trustline);
3235 BEAST_EXPECT(sleLine2 == nullptr);
3236
3237 // Burn some XRP
3238 env(noop(lender), Fee(XRP(incReserve)));
3239 env.close();
3240
3241 // Cannot create loan, not enough reserve to create trust line
3242 env(set(borrower, broker.brokerID, principalRequest),
3243 kLoanOriginationFee(broker.asset(1).value()),
3244 kCounterparty(lender),
3245 Sig(sfCounterpartySignature, lender),
3246 Fee(env.current()->fees().base * 5),
3247 Ter{tecNO_LINE_INSUF_RESERVE});
3248 env.close();
3249
3250 // Can create loan now, will implicitly create trust line
3251 env(pay(issuer, lender, XRP(incReserve)));
3252 env.close();
3253 env(set(borrower, broker.brokerID, principalRequest),
3254 kLoanOriginationFee(broker.asset(1).value()),
3255 kCounterparty(lender),
3256 Sig(sfCounterpartySignature, lender),
3257 Fee(env.current()->fees().base * 5));
3258 env.close();
3259
3260 auto const sleLine3 = env.le(trustline);
3261 BEAST_EXPECT(sleLine3 != nullptr);
3262 },
3263 CaseArgs{.initialXRP = (acctReserve * 2) + (incReserve * 8) + 1});
3264
3265 testCase(
3266 [&, this](Env& env, BrokerInfo const& broker, MPTTester& mptt) {
3267 using namespace loan;
3268 Number const principalRequest = broker.asset(1'000).value();
3269
3270 testcase("MPT authorized borrower, unauthorized lender");
3271 auto const mptoken = keylet::mptoken(mptt.issuanceID(), lender);
3272 auto const sleMPT1 = env.le(mptoken);
3273 BEAST_EXPECT(sleMPT1 != nullptr);
3274
3275 env(pay(lender, issuer, broker.asset(sleMPT1->at(sfMPTAmount))));
3276 env.close();
3277
3278 mptt.authorize({.account = lender, .flags = tfMPTUnauthorize});
3279 env.close();
3280
3281 auto const sleMPT2 = env.le(mptoken);
3282 BEAST_EXPECT(sleMPT2 == nullptr);
3283
3284 // Cannot create loan, lender not authorized to receive fee
3285 env(set(borrower, broker.brokerID, principalRequest),
3286 kLoanOriginationFee(broker.asset(1).value()),
3287 kCounterparty(lender),
3288 Sig(sfCounterpartySignature, lender),
3289 Fee(env.current()->fees().base * 5),
3290 Ter{tecNO_AUTH});
3291 env.close();
3292
3293 // Cannot create loan, even without an origination fee
3294 env(set(borrower, broker.brokerID, principalRequest),
3295 kCounterparty(lender),
3296 Sig(sfCounterpartySignature, lender),
3297 Fee(env.current()->fees().base * 5),
3298 Ter{tecNO_AUTH});
3299 env.close();
3300
3301 // No MPToken for lender - no authorization and no payment
3302 auto const sleMPT3 = env.le(mptoken);
3303 BEAST_EXPECT(sleMPT3 == nullptr);
3304 },
3305 {},
3306 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3307
3308 testCase(
3309 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3310 using namespace loan;
3311 Number const principalRequest = broker.asset(1'000).value();
3312
3313 testcase("MPT authorized borrower, borrower submits");
3314 env(set(borrower, broker.brokerID, principalRequest),
3315 kCounterparty(lender),
3316 Sig(sfCounterpartySignature, lender),
3317 Fee(env.current()->fees().base * 5));
3318 },
3319 [&, this](Env& env, BrokerInfo const& broker) {
3320 using namespace loan;
3321 Number const principalRequest = broker.asset(1'000).value();
3322
3323 testcase("IOU authorized borrower, borrower submits");
3324 env(set(borrower, broker.brokerID, principalRequest),
3325 kCounterparty(lender),
3326 Sig(sfCounterpartySignature, lender),
3327 Fee(env.current()->fees().base * 5));
3328 },
3329 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3330
3331 testCase(
3332 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3333 using namespace loan;
3334 Number const principalRequest = broker.asset(1'000).value();
3335
3336 testcase("MPT authorized borrower, lender submits");
3337 env(set(lender, broker.brokerID, principalRequest),
3338 kCounterparty(borrower),
3339 Sig(sfCounterpartySignature, borrower),
3340 Fee(env.current()->fees().base * 5));
3341 },
3342 [&, this](Env& env, BrokerInfo const& broker) {
3343 using namespace loan;
3344 Number const principalRequest = broker.asset(1'000).value();
3345
3346 testcase("IOU authorized borrower, lender submits");
3347 env(set(lender, broker.brokerID, principalRequest),
3348 kCounterparty(borrower),
3349 Sig(sfCounterpartySignature, borrower),
3350 Fee(env.current()->fees().base * 5));
3351 },
3352 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3353
3354 jtx::Account const alice{"alice"};
3355 jtx::Account const bella{"bella"};
3356 auto const msigSetup = [&](Env& env, Account const& account) {
3357 json::Value const tx1 = signers(account, 2, {{alice, 1}, {bella, 1}});
3358 env(tx1);
3359 env.close();
3360 };
3361
3362 testCase(
3363 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3364 using namespace loan;
3365 msigSetup(env, lender);
3366 Number const principalRequest = broker.asset(1'000).value();
3367
3368 testcase(
3369 "MPT authorized borrower, borrower submits, lender "
3370 "multisign");
3371 env(set(borrower, broker.brokerID, principalRequest),
3372 kCounterparty(lender),
3373 Msig(sfCounterpartySignature, alice, bella),
3374 Fee(env.current()->fees().base * 5));
3375 },
3376 [&, this](Env& env, BrokerInfo const& broker) {
3377 using namespace loan;
3378 msigSetup(env, lender);
3379 Number const principalRequest = broker.asset(1'000).value();
3380
3381 testcase(
3382 "IOU authorized borrower, borrower submits, lender "
3383 "multisign");
3384 env(set(borrower, broker.brokerID, principalRequest),
3385 kCounterparty(lender),
3386 Msig(sfCounterpartySignature, alice, bella),
3387 Fee(env.current()->fees().base * 5));
3388 },
3389 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3390
3391 testCase(
3392 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3393 using namespace loan;
3394 msigSetup(env, borrower);
3395 Number const principalRequest = broker.asset(1'000).value();
3396
3397 testcase(
3398 "MPT authorized borrower, lender submits, borrower "
3399 "multisign");
3400 env(set(lender, broker.brokerID, principalRequest),
3401 kCounterparty(borrower),
3402 Msig(sfCounterpartySignature, alice, bella),
3403 Fee(env.current()->fees().base * 5));
3404 },
3405 [&, this](Env& env, BrokerInfo const& broker) {
3406 using namespace loan;
3407 msigSetup(env, borrower);
3408 Number const principalRequest = broker.asset(1'000).value();
3409
3410 testcase(
3411 "IOU authorized borrower, lender submits, borrower "
3412 "multisign");
3413 env(set(lender, broker.brokerID, principalRequest),
3414 kCounterparty(borrower),
3415 Msig(sfCounterpartySignature, alice, bella),
3416 Fee(env.current()->fees().base * 5));
3417 },
3418 CaseArgs{.requireAuth = true, .authorizeBorrower = true});
3419
3420 testCase(
3421 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3422 using namespace loan;
3423 Number const principalRequest = broker.asset(1'000).value();
3424 Vault const vault{env};
3425 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
3426 tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit;
3427 env(tx);
3428 env.close();
3429
3430 testcase("Vault at maximum value");
3431 env(set(issuer, broker.brokerID, principalRequest),
3432 kCounterparty(lender),
3433 kInterestRate(TenthBips32(10'000)),
3434 Sig(sfCounterpartySignature, lender),
3435 Fee(env.current()->fees().base * 5),
3437 },
3438 nullptr);
3439
3440 testCase(
3441 [&, this](Env& env, BrokerInfo const& broker, auto&) {
3442 using namespace loan;
3443 Number const principalRequest = broker.asset(1'000).value();
3444 Vault const vault{env};
3445 auto tx = vault.set({.owner = lender, .id = broker.vaultID});
3446 tx[sfAssetsMaximum] =
3447 BrokerParameters::defaults().vaultDeposit + broker.asset(1).number();
3448 env(tx);
3449 env.close();
3450
3451 testcase("Vault maximum value exceeded");
3452 env(set(issuer, broker.brokerID, principalRequest),
3453 kCounterparty(lender),
3454 kInterestRate(TenthBips32(100'000)),
3455 Sig(sfCounterpartySignature, lender),
3456 Fee(env.current()->fees().base * 5),
3457 kPaymentTotal(2),
3458 kPaymentInterval(3600 * 24),
3460 },
3461 nullptr);
3462 }
3463
3464 void
3466 {
3467 testcase("Lifecycle");
3468 using namespace jtx;
3469
3470 // Create 3 loan brokers: one for XRP, one for an IOU, and one for
3471 // an MPT. That'll require three corresponding SAVs.
3472 Env env(*this, features);
3473
3474 Account const issuer{"issuer"};
3475 // For simplicity, lender will be the sole actor for the vault &
3476 // brokers.
3477 Account const lender{"lender"};
3478 // Borrower only wants to borrow
3479 Account const borrower{"borrower"};
3480 // Evan will attempt to be naughty
3481 Account const evan{"evan"};
3482 // Do not fund alice
3483 Account const alice{"alice"};
3484
3485 // Fund the accounts and trust lines with the same amount so that
3486 // tests can use the same values regardless of the asset.
3487 env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan));
3488 env.close();
3489
3490 // Create assets
3491 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3492 PrettyAsset const iouAsset = issuer[iouCurrency_];
3493 env(trust(lender, iouAsset(10'000'000)));
3494 env(trust(borrower, iouAsset(10'000'000)));
3495 env(trust(evan, iouAsset(10'000'000)));
3496 env(pay(issuer, evan, iouAsset(1'000'000)));
3497 env(pay(issuer, lender, iouAsset(10'000'000)));
3498 // Fund the borrower with enough to cover interest and fees
3499 env(pay(issuer, borrower, iouAsset(10'000)));
3500 env.close();
3501
3502 MPTTester mptt{env, issuer, kMptInitNoFund};
3503 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
3504 // Scale the MPT asset a little bit so we can get some interest
3505 PrettyAsset const mptAsset{mptt.issuanceID(), 100};
3506 mptt.authorize({.account = lender});
3507 mptt.authorize({.account = borrower});
3508 mptt.authorize({.account = evan});
3509 env(pay(issuer, lender, mptAsset(10'000'000)));
3510 env(pay(issuer, evan, mptAsset(1'000'000)));
3511 // Fund the borrower with enough to cover interest and fees
3512 env(pay(issuer, borrower, mptAsset(10'000)));
3513 env.close();
3514
3515 std::array const assets{iouAsset, xrpAsset, mptAsset};
3516
3517 // Create vaults and loan brokers
3519 brokers.reserve(assets.size());
3520 for (auto const& asset : assets)
3521 {
3523 env, asset, lender, BrokerParameters{.data = "spam spam spam spam"}));
3524 }
3525
3526 // Create and update Loans
3527 for (auto const& broker : brokers)
3528 {
3529 for (int amountExponent = 3; amountExponent >= 3; --amountExponent)
3530 {
3531 Number const loanAmount{1, amountExponent};
3532 for (int interestExponent = 0; interestExponent >= 0; --interestExponent)
3533 {
3534 testCaseWrapper(env, mptt, assets, broker, loanAmount, interestExponent);
3535 }
3536 }
3537
3538 if (auto brokerSle = env.le(keylet::loanBroker(broker.brokerID));
3539 BEAST_EXPECT(brokerSle))
3540 {
3541 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
3542 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
3543
3544 auto const coverAvailable = brokerSle->at(sfCoverAvailable);
3546 lender, broker.brokerID, STAmount(broker.asset, coverAvailable)));
3547 env.close();
3548
3549 brokerSle = env.le(keylet::loanBroker(broker.brokerID));
3550 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
3551 }
3552 // Verify we can delete the loan broker
3553 env(loanBroker::del(lender, broker.brokerID));
3554 env.close();
3555 }
3556 }
3557
3558 void
3560 {
3561 testcase << "Self Loan";
3562
3563 using namespace jtx;
3564 using namespace std::chrono_literals;
3565 // Create 3 loan brokers: one for XRP, one for an IOU, and one for
3566 // an MPT. That'll require three corresponding SAVs.
3567 Env env(*this, features);
3568
3569 Account const issuer{"issuer"};
3570 // For simplicity, lender will be the sole actor for the vault &
3571 // brokers.
3572 Account const lender{"lender"};
3573
3574 // Fund the accounts and trust lines with the same amount so that
3575 // tests can use the same values regardless of the asset.
3576 env.fund(XRP(100'000'000), issuer, noripple(lender));
3577 env.close();
3578
3579 // Use an XRP asset for simplicity
3580 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3581
3582 // Create vaults and loan brokers
3583 BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)};
3584
3585 using namespace loan;
3586
3587 auto const loanSetFee = Fee(env.current()->fees().base * 2);
3588 Number const principalRequest{1, 3};
3589
3590 // The LoanSet json can be created without a counterparty signature,
3591 // but it will not pass preflight
3592 auto createJson = env.json(
3593 set(lender, broker.brokerID, broker.asset(principalRequest).value()), Fee(loanSetFee));
3594 env(createJson, Ter(temBAD_SIGNER));
3595
3596 // Adding an empty counterparty signature object also fails, but
3597 // at the RPC level.
3598 createJson = env.json(createJson, Json(sfCounterpartySignature, json::ValueType::Object));
3599 env(createJson, Ter(telENV_RPC_FAILED));
3600
3601 if (auto const jt = env.jt(createJson); BEAST_EXPECT(jt.stx))
3602 {
3603 Serializer s;
3604 jt.stx->add(s);
3605 auto const jr = env.rpc("submit", strHex(s.slice()));
3606
3607 BEAST_EXPECT(jr.isMember(jss::result));
3608 auto const jResult = jr[jss::result];
3609 BEAST_EXPECT(jResult[jss::error] == "invalidTransaction");
3610 BEAST_EXPECT(
3611 jResult[jss::error_exception] ==
3612 "fails local checks: Transaction has bad signature.");
3613 }
3614
3615 // Copy the transaction signature into the counterparty signature.
3616 json::Value counterpartyJson{json::ValueType::Object};
3617 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
3618 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
3619 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
3620 counterpartyJson[sfSigners] = createJson[sfSigners];
3621
3622 // The duplicated signature works
3623 createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson));
3624 env(createJson);
3625
3626 env.close();
3627
3628 auto const startDate = env.current()->header().parentCloseTime;
3629
3630 // Loan is successfully created
3631 {
3632 auto const res = env.rpc("account_objects", lender.human());
3633 auto const objects = res[jss::result][jss::account_objects];
3634
3636 BEAST_EXPECT(objects.size() == 4);
3637 for (auto const& object : objects)
3638 {
3639 ++types[object[sfLedgerEntryType].asString()];
3640 }
3641 BEAST_EXPECT(types.size() == 4);
3642 for (std::string const type : {"MPToken", "Vault", "LoanBroker", "Loan"})
3643 {
3644 BEAST_EXPECT(types[type] == 1);
3645 }
3646 }
3647 auto const loanID = [&]() {
3649 params[jss::account] = lender.human();
3650 params[jss::type] = "Loan";
3651 auto const res = env.rpc("json", "account_objects", to_string(params));
3652 auto const objects = res[jss::result][jss::account_objects];
3653
3654 BEAST_EXPECT(objects.size() == 1);
3655
3656 auto const loan = objects[0u];
3657 BEAST_EXPECT(loan[sfBorrower] == lender.human());
3658 // soeDEFAULT fields are not returned if they're in the default
3659 // state
3660 BEAST_EXPECT(!loan.isMember(sfCloseInterestRate));
3661 BEAST_EXPECT(!loan.isMember(sfClosePaymentFee));
3662 BEAST_EXPECT(loan[sfFlags] == 0);
3663 BEAST_EXPECT(loan[sfGracePeriod] == 60);
3664 BEAST_EXPECT(!loan.isMember(sfInterestRate));
3665 BEAST_EXPECT(!loan.isMember(sfLateInterestRate));
3666 BEAST_EXPECT(!loan.isMember(sfLatePaymentFee));
3667 BEAST_EXPECT(loan[sfLoanBrokerID] == to_string(broker.brokerID));
3668 BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee));
3669 BEAST_EXPECT(loan[sfLoanSequence] == 1);
3670 BEAST_EXPECT(!loan.isMember(sfLoanServiceFee));
3671 BEAST_EXPECT(loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60);
3672 BEAST_EXPECT(!loan.isMember(sfOverpaymentFee));
3673 BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate));
3674 BEAST_EXPECT(loan[sfPaymentInterval] == 60);
3675 BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000");
3676 BEAST_EXPECT(loan[sfPaymentRemaining] == 1);
3677 BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate));
3678 BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000");
3679 BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000");
3680 BEAST_EXPECT(!loan.isMember(sfLoanScale));
3681 BEAST_EXPECT(loan[sfStartDate].asUInt() == startDate.time_since_epoch().count());
3682
3683 return loan["index"].asString();
3684 }();
3685 auto const loanKeylet{keylet::loan(uint256{std::string_view(loanID)})};
3686
3687 env.close(startDate);
3688
3689 // Make a payment
3690 env(pay(lender, loanKeylet.key, broker.asset(1000)));
3691 }
3692
3693 void
3695 {
3696 // From FIND-001
3697 testcase << "Batch Bypass Counterparty";
3698
3699 bool const lendingBatchEnabled = !std::ranges::any_of(
3701 [](auto const& disabled) { return disabled == ttLOAN_BROKER_SET; });
3702
3703 using namespace jtx;
3704 using namespace std::chrono_literals;
3705 Env env(*this, features);
3706
3707 Account const lender{"lender"};
3708 Account const borrower{"borrower"};
3709
3710 BrokerParameters const brokerParams;
3711 env.fund(XRP(brokerParams.vaultDeposit * 100), lender, borrower);
3712 env.close();
3713
3714 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3715
3716 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3717
3718 using namespace loan;
3719
3720 auto const loanSetFee = Fee(env.current()->fees().base * 2);
3721 Number const principalRequest{1, 3};
3722
3723 auto forgedLoanSet = set(borrower, broker.brokerID, principalRequest, 0);
3724
3726 randomData[jss::SigningPubKey] = json::StaticString{"2600"};
3728 sigObject[jss::SigningPubKey] = strHex(lender.pk().slice());
3729 Serializer ss;
3731 parse(randomData).addWithoutSigningFields(ss);
3732 auto const sig = xrpl::sign(borrower.pk(), borrower.sk(), ss.slice());
3733 sigObject[jss::TxnSignature] = strHex(Slice{sig.data(), sig.size()});
3734
3735 forgedLoanSet[json::StaticString{"CounterpartySignature"}] = sigObject;
3736
3737 // ? Fails because the lender hasn't signed the tx
3738 env(env.json(forgedLoanSet, Fee(loanSetFee)), Ter(telENV_RPC_FAILED));
3739
3740 auto const seq = env.seq(borrower);
3741 auto const batchFee = batch::calcBatchFee(env, 1, 2);
3742 // ! Should fail because the lender hasn't signed the tx
3743 env(batch::outer(borrower, seq, batchFee, tfAllOrNothing),
3744 batch::Inner(forgedLoanSet, seq + 1),
3745 batch::Inner(pay(borrower, lender, XRP(1)), seq + 2),
3746 Ter(lendingBatchEnabled ? temBAD_SIGNATURE : temINVALID_INNER_BATCH));
3747 env.close();
3748
3749 // ? Check that the loan was NOT created
3750 {
3752 params[jss::account] = borrower.human();
3753 params[jss::type] = "Loan";
3754 auto const res = env.rpc("json", "account_objects", to_string(params));
3755 auto const objects = res[jss::result][jss::account_objects];
3756 BEAST_EXPECT(objects.size() == 0);
3757 }
3758 }
3759
3760 void
3762 {
3763 // From FIND-003
3764 testcase << "Wrong Max Debt Behavior";
3765
3766 using namespace jtx;
3767 using namespace std::chrono_literals;
3768 Env env(*this, features);
3769
3770 Account const issuer{"issuer"};
3771 Account const lender{"lender"};
3772
3773 BrokerParameters const brokerParams{.debtMax = 0};
3774 env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, noripple(lender));
3775 env.close();
3776
3777 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3778
3779 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3780
3781 if (auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
3782 BEAST_EXPECT(brokerSle))
3783 {
3784 BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0);
3785 }
3786
3787 using namespace loan;
3788
3789 auto const loanSetFee = Fee(env.current()->fees().base * 2);
3790 Number const principalRequest{1, 3};
3791
3792 auto createJson = env.json(set(lender, broker.brokerID, principalRequest), Fee(loanSetFee));
3793
3794 json::Value counterpartyJson{json::ValueType::Object};
3795 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
3796 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
3797 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
3798 counterpartyJson[sfSigners] = createJson[sfSigners];
3799
3800 createJson = env.json(createJson, Json(sfCounterpartySignature, counterpartyJson));
3801 env(createJson);
3802
3803 env.close();
3804 }
3805
3806 void
3808 {
3809 // From FIND-012
3810 testcase << "LoanPay xrpl::detail::computePeriodicPayment : "
3811 "valid rate";
3812
3813 using namespace jtx;
3814 using namespace std::chrono_literals;
3815 Env env(*this, features);
3816
3817 Account const issuer{"issuer"};
3818 Account const lender{"lender"};
3819 Account const borrower{"borrower"};
3820
3821 BrokerParameters const brokerParams;
3822 env.fund(XRP(brokerParams.vaultDeposit * 100), issuer, lender, borrower);
3823 env.close();
3824
3825 PrettyAsset const xrpAsset{xrpIssue(), 1'000'000};
3826 BrokerInfo const broker{createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3827
3828 using namespace loan;
3829
3830 auto const loanSetFee = Fee(env.current()->fees().base * 2);
3831 Number const principalRequest{640562, -5};
3832
3833 Number const serviceFee{2462611968};
3834 std::uint32_t const numPayments{4294967295 / 800};
3835
3836 auto createJson = env.json(
3837 set(borrower, broker.brokerID, principalRequest),
3838 Fee(loanSetFee),
3839 kLoanServiceFee(serviceFee),
3840 kPaymentTotal(numPayments),
3841 Json(sfCounterpartySignature, json::ValueType::Object));
3842
3843 createJson["CloseInterestRate"] = 55374;
3844 createJson["ClosePaymentFee"] = "3825205248";
3845 createJson["LatePaymentFee"] = "237";
3846 createJson["LoanOriginationFee"] = "0";
3847 createJson["OverpaymentFee"] = 35167;
3848 createJson["OverpaymentInterestRate"] = 1360;
3849 createJson["PaymentInterval"] = 727;
3850
3851 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
3852 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
3853 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
3854
3855 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
3856 // Fails in preclaim because principal requested can't be
3857 // represented as XRP
3858 env(createJson, Ter(tecPRECISION_LOSS));
3859 env.close();
3860
3861 BEAST_EXPECT(!env.le(keylet));
3862
3863 Number const actualPrincipal{6};
3864
3865 createJson[sfPrincipalRequested] = actualPrincipal;
3866 createJson.removeMember(sfSequence.jsonName);
3867 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
3868 // Fails in doApply because the payment is too small to be
3869 // represented as XRP.
3870 env(createJson, Ter(tecPRECISION_LOSS));
3871 env.close();
3872 }
3873
3874 void
3876 {
3877 // This will expand as more test cases are added. Some functionality
3878 // is tested in other test functions.
3879 testcase("RPC");
3880
3881 using namespace jtx;
3882
3883 Env env(*this, features);
3884
3885 auto lowerFee = [&]() {
3886 // Run the local fee back down.
3887 while (env.app().getFeeTrack().lowerLocalFee())
3888 ;
3889 };
3890
3891 auto const baseFee = env.current()->fees().base;
3892
3893 Account const alice{"alice"};
3894 std::string const borrowerPass = "borrower";
3895 Account const borrower{borrowerPass, KeyType::Ed25519};
3896 auto const lenderPass = "lender";
3897 Account const lender{lenderPass, KeyType::Ed25519};
3898
3899 env.fund(XRP(1'000'000), alice, lender, borrower);
3900 env.close();
3901 env(noop(lender));
3902 env(noop(lender));
3903 env(noop(lender));
3904 env(noop(lender));
3905 env(noop(lender));
3906 env.close();
3907
3908 {
3909 testcase("RPC AccountSet");
3911 txJson[sfTransactionType] = "AccountSet";
3912 txJson[sfAccount] = borrower.human();
3913
3914 auto const signParams = [&]() {
3916 signParams[jss::passphrase] = borrowerPass;
3917 signParams[jss::key_type] = "ed25519";
3918 signParams[jss::tx_json] = txJson;
3919 return signParams;
3920 }();
3921 auto const jSign = env.rpc("json", "sign", to_string(signParams));
3922 BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json));
3923 auto txSignResult = jSign[jss::result][jss::tx_json];
3924 auto txSignBlob = jSign[jss::result][jss::tx_blob].asString();
3925 txSignResult.removeMember(jss::hash);
3926
3927 auto const jtx = env.jt(txJson, Sig(borrower));
3928 BEAST_EXPECT(txSignResult == jtx.jv);
3929
3930 lowerFee();
3931 auto const jSubmit = env.rpc("submit", txSignBlob);
3932 BEAST_EXPECT(
3933 jSubmit.isMember(jss::result) &&
3934 jSubmit[jss::result].isMember(jss::engine_result) &&
3935 jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS");
3936
3937 lowerFee();
3938 env(jtx.jv, Sig(kNone), Seq(kNone), Fee(kNone), Ter(tefPAST_SEQ));
3939 }
3940
3941 {
3942 testcase("RPC LoanSet - illegal signature_target");
3943
3945 txJson[sfTransactionType] = "AccountSet";
3946 txJson[sfAccount] = borrower.human();
3947
3948 auto const borrowerSignParams = [&]() {
3950 params[jss::passphrase] = borrowerPass;
3951 params[jss::key_type] = "ed25519";
3952 params[jss::signature_target] = "Destination";
3953 params[jss::tx_json] = txJson;
3954 return params;
3955 }();
3956 auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
3957 BEAST_EXPECT(
3958 jSignBorrower.isMember(jss::result) &&
3959 jSignBorrower[jss::result].isMember(jss::error) &&
3960 jSignBorrower[jss::result][jss::error] == "invalidParams" &&
3961 jSignBorrower[jss::result].isMember(jss::error_message) &&
3962 jSignBorrower[jss::result][jss::error_message] == "Destination");
3963 }
3964 {
3965 testcase("RPC LoanSet - sign and submit borrower initiated");
3966 // 1. Borrower creates the transaction
3968 txJson[sfTransactionType] = "LoanSet";
3969 txJson[sfAccount] = borrower.human();
3970 txJson[sfCounterparty] = lender.human();
3971 txJson[sfLoanBrokerID] =
3972 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
3973 "F83F"
3974 "5C";
3975 txJson[sfPrincipalRequested] = "100000000";
3976 txJson[sfPaymentTotal] = 10000;
3977 txJson[sfPaymentInterval] = 3600;
3978 txJson[sfGracePeriod] = 300;
3979 txJson[sfFlags] = 65536; // tfLoanOverpayment
3980 txJson[sfFee] = to_string(24 * baseFee / 10);
3981
3982 // 2. Borrower signs the transaction
3983 auto const borrowerSignParams = [&]() {
3985 params[jss::passphrase] = borrowerPass;
3986 params[jss::key_type] = "ed25519";
3987 params[jss::tx_json] = txJson;
3988 return params;
3989 }();
3990 auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
3991 BEAST_EXPECTS(
3992 jSignBorrower.isMember(jss::result) &&
3993 jSignBorrower[jss::result].isMember(jss::tx_json),
3994 to_string(jSignBorrower));
3995 auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json];
3996 auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString();
3997
3998 // 2a. Borrower attempts to submit the transaction. It doesn't
3999 // work
4000 {
4001 lowerFee();
4002 auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob);
4003 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4004 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4005 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4006 // Transaction fails because the CounterpartySignature is
4007 // missing
4008 BEAST_EXPECT(
4009 jSubmitBlobResult.isMember(jss::engine_result) &&
4010 jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER");
4011 }
4012
4013 // 3. Borrower sends the signed transaction to the lender
4014 // 4. Lender signs the transaction
4015 auto const lenderSignParams = [&]() {
4017 params[jss::passphrase] = lenderPass;
4018 params[jss::key_type] = "ed25519";
4019 params[jss::signature_target] = "CounterpartySignature";
4020 params[jss::tx_json] = txBorrowerSignResult;
4021 return params;
4022 }();
4023 auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams));
4024 BEAST_EXPECT(
4025 jSignLender.isMember(jss::result) &&
4026 jSignLender[jss::result].isMember(jss::tx_json));
4027 auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json];
4028 auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString();
4029
4030 // 5. Lender submits the signed transaction blob
4031 lowerFee();
4032 auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob);
4033 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4034 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4035 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4036 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
4037 // To get far enough to return tecNO_ENTRY means that the
4038 // signatures all validated. Of course the transaction won't
4039 // succeed because no Vault or Broker were created.
4040 BEAST_EXPECTS(
4041 jSubmitBlobResult.isMember(jss::engine_result) &&
4042 jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY",
4043 to_string(jSubmitBlobResult));
4044
4045 BEAST_EXPECT(
4046 !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error));
4047
4048 // 4-alt. Lender submits the transaction json originally
4049 // received from the Borrower. It gets signed, but is now a
4050 // duplicate, so fails. Borrower could done this instead of
4051 // steps 4 and 5.
4052 lowerFee();
4053 auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams));
4054 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
4055 auto const jSubmitJsonResult = jSubmitJson[jss::result];
4056 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
4057 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
4058 // Since the previous tx claimed a fee, this duplicate is not
4059 // going anywhere
4060 BEAST_EXPECTS(
4061 jSubmitJsonResult.isMember(jss::engine_result) &&
4062 jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ",
4063 to_string(jSubmitJsonResult));
4064
4065 BEAST_EXPECT(
4066 !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error));
4067
4068 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
4069 }
4070
4071 {
4072 testcase("RPC LoanSet - sign and submit lender initiated");
4073 // 1. Lender creates the transaction
4075 txJson[sfTransactionType] = "LoanSet";
4076 txJson[sfAccount] = lender.human();
4077 txJson[sfCounterparty] = borrower.human();
4078 txJson[sfLoanBrokerID] =
4079 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
4080 "F83F"
4081 "5C";
4082 txJson[sfPrincipalRequested] = "100000000";
4083 txJson[sfPaymentTotal] = 10000;
4084 txJson[sfPaymentInterval] = 3600;
4085 txJson[sfGracePeriod] = 300;
4086 txJson[sfFlags] = 65536; // tfLoanOverpayment
4087 txJson[sfFee] = to_string(24 * baseFee / 10);
4088
4089 // 2. Lender signs the transaction
4090 auto const lenderSignParams = [&]() {
4092 params[jss::passphrase] = lenderPass;
4093 params[jss::key_type] = "ed25519";
4094 params[jss::tx_json] = txJson;
4095 return params;
4096 }();
4097 auto const jSignLender = env.rpc("json", "sign", to_string(lenderSignParams));
4098 BEAST_EXPECT(
4099 jSignLender.isMember(jss::result) &&
4100 jSignLender[jss::result].isMember(jss::tx_json));
4101 auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json];
4102 auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString();
4103
4104 // 2a. Lender attempts to submit the transaction. It doesn't
4105 // work
4106 {
4107 lowerFee();
4108 auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob);
4109 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4110 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4111 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4112 // Transaction fails because the CounterpartySignature is
4113 // missing
4114 BEAST_EXPECT(
4115 jSubmitBlobResult.isMember(jss::engine_result) &&
4116 jSubmitBlobResult[jss::engine_result].asString() == "temBAD_SIGNER");
4117 }
4118
4119 // 3. Lender sends the signed transaction to the Borrower
4120 // 4. Borrower signs the transaction
4121 auto const borrowerSignParams = [&]() {
4123 params[jss::passphrase] = borrowerPass;
4124 params[jss::key_type] = "ed25519";
4125 params[jss::signature_target] = "CounterpartySignature";
4126 params[jss::tx_json] = txLenderSignResult;
4127 return params;
4128 }();
4129 auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams));
4130 BEAST_EXPECT(
4131 jSignBorrower.isMember(jss::result) &&
4132 jSignBorrower[jss::result].isMember(jss::tx_json));
4133 auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json];
4134 auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString();
4135
4136 // 5. Borrower submits the signed transaction blob
4137 lowerFee();
4138 auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob);
4139 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4140 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4141 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4142 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
4143 // To get far enough to return tecNO_ENTRY means that the
4144 // signatures all validated. Of course the transaction won't
4145 // succeed because no Vault or Broker were created.
4146 BEAST_EXPECTS(
4147 jSubmitBlobResult.isMember(jss::engine_result) &&
4148 jSubmitBlobResult[jss::engine_result].asString() == "tecNO_ENTRY",
4149 to_string(jSubmitBlobResult));
4150
4151 BEAST_EXPECT(
4152 !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error));
4153
4154 // 4-alt. Borrower submits the transaction json originally
4155 // received from the Lender. It gets signed, but is now a
4156 // duplicate, so fails. Lender could done this instead of steps
4157 // 4 and 5.
4158 lowerFee();
4159 auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams));
4160 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
4161 auto const jSubmitJsonResult = jSubmitJson[jss::result];
4162 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
4163 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
4164 // Since the previous tx claimed a fee, this duplicate is not
4165 // going anywhere
4166 BEAST_EXPECTS(
4167 jSubmitJsonResult.isMember(jss::engine_result) &&
4168 jSubmitJsonResult[jss::engine_result].asString() == "tefPAST_SEQ",
4169 to_string(jSubmitJsonResult));
4170
4171 BEAST_EXPECT(
4172 !jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error));
4173
4174 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
4175 }
4176 }
4177
4178 void
4180 {
4181 testcase << "Service Fee On Broker Deep Freeze";
4182 using namespace jtx;
4183 using namespace loan;
4184 Account const issuer("issuer");
4185 Account const borrower("borrower");
4186 Account const broker("broker");
4187 auto const iou = issuer["IOU"];
4188
4189 for (bool const deepFreeze : {true, false})
4190 {
4191 Env env(*this);
4192
4193 auto getCoverBalance = [&](BrokerInfo const& brokerInfo, auto const& accountField) {
4194 if (auto const le = env.le(keylet::loanBroker(brokerInfo.brokerID));
4195 BEAST_EXPECT(le))
4196 {
4197 auto const account = le->at(accountField);
4198 if (auto const sleLine = env.le(keylet::trustLine(account, iou));
4199 BEAST_EXPECT(sleLine))
4200 {
4201 STAmount balance = sleLine->at(sfBalance);
4202 if (account > issuer.id())
4203 balance.negate();
4204 return balance;
4205 }
4206 }
4207 return STAmount{iou};
4208 };
4209
4210 env.fund(XRP(20'000), issuer, broker, borrower);
4211 env.close();
4212
4213 env(trust(broker, iou(20'000'000)));
4214 env(pay(issuer, broker, iou(10'000'000)));
4215 env.close();
4216
4217 auto const brokerInfo = createVaultAndBroker(env, iou, broker);
4218
4219 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
4220
4221 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
4222
4223 env(set(borrower, brokerInfo.brokerID, 10'000),
4224 Sig(sfCounterpartySignature, broker),
4225 kLoanServiceFee(iou(100).value()),
4226 kPaymentInterval(100),
4227 Fee(XRP(100)));
4228 env.close();
4229
4230 env(trust(borrower, iou(20'000'000)));
4231 // The borrower increases their limit and acquires some IOU so
4232 // they can pay interest
4233 env(pay(issuer, borrower, iou(500)));
4234 env.close();
4235
4236 if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le))
4237 {
4238 if (deepFreeze)
4239 {
4240 env(trust(issuer, broker["IOU"](0), tfSetFreeze | tfSetDeepFreeze));
4241 env.close();
4242 }
4243
4244 env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)));
4245 env.close();
4246
4247 if (deepFreeze)
4248 {
4249 // The fee goes to the broker pseudo-account
4250 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'100));
4251 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'000));
4252 }
4253 else
4254 {
4255 // The fee goes to the broker account
4256 BEAST_EXPECT(getCoverBalance(brokerInfo, sfOwner) == iou(8'999'100));
4257 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) == iou(1'000));
4258 }
4259 }
4260 };
4261 }
4262
4263 void
4265 {
4266 testcase << "Issuer Loan";
4267
4268 using namespace jtx;
4269 using namespace loan;
4270 Account const issuer("issuer");
4271 Account const borrower = issuer;
4272 Account const lender("lender");
4273 Env env(*this);
4274
4275 env.fund(XRP(1'000), issuer, lender);
4276
4277 static constexpr std::int64_t kIssuerBalance = 10'000'000;
4278 MPTTester const asset(
4279 {.env = env, .issuer = issuer, .holders = {lender}, .pay = kIssuerBalance});
4280
4281 BrokerParameters const brokerParams{
4282 .debtMax = 200,
4283 };
4284 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
4285 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4286 // Create Loan
4287 env(set(borrower, broker.brokerID, 200), Sig(sfCounterpartySignature, lender), loanSetFee);
4288 env.close();
4289 // Issuer should not create MPToken
4290 BEAST_EXPECT(!env.le(keylet::mptoken(asset.issuanceID(), issuer)));
4291 // Issuer "borrowed" 200, OutstandingAmount decreased by 200
4292 BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance + 200));
4293 // Pay Loan
4294 auto const loanKeylet = keylet::loan(broker.brokerID, 1);
4295 env(pay(borrower, loanKeylet.key, asset(200)));
4296 env.close();
4297 // Issuer "re-payed" 200, OutstandingAmount increased by 200
4298 BEAST_EXPECT(env.balance(issuer, asset) == asset(-kIssuerBalance));
4299 }
4300
4301 void
4303 {
4304 testcase("Invalid LoanDelete");
4305 using namespace jtx;
4306 using namespace loan;
4307
4308 // preflight: temINVALID, LoanID == zero
4309 {
4310 Account const alice{"alice"};
4311 Env env(*this);
4312 env.fund(XRP(1'000), alice);
4313 env.close();
4314 env(del(alice, beast::kZero), Ter(temINVALID));
4315 }
4316 }
4317
4318 void
4320 {
4321 testcase("Invalid LoanManage");
4322 using namespace jtx;
4323 using namespace loan;
4324
4325 // preflight: temINVALID, LoanID == zero
4326 {
4327 Account const alice{"alice"};
4328 Env env(*this);
4329 env.fund(XRP(1'000), alice);
4330 env.close();
4331 env(manage(alice, beast::kZero, tfLoanDefault), Ter(temINVALID));
4332 }
4333 }
4334
4335 void
4337 {
4338 testcase("Invalid LoanPay");
4339 using namespace jtx;
4340 using namespace loan;
4341 Account const lender{"lender"};
4342 Account const issuer{"issuer"};
4343 Account const borrower{"borrower"};
4344 auto const iou = issuer["IOU"];
4345
4346 // preclaim
4347 Env env(*this);
4348 env.fund(XRP(1'000), lender, issuer, borrower);
4349 env(trust(lender, iou(10'000'000)));
4350 env(pay(issuer, lender, iou(5'000'000)));
4351 BrokerInfo brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
4352
4353 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4354 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
4355
4356 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4357 Sig(sfCounterpartySignature, lender),
4358 loanSetFee);
4359
4360 env.close();
4361
4362 std::uint32_t const loanSequence = 1;
4363 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
4364
4365 env(fset(issuer, asfGlobalFreeze));
4366 env.close();
4367
4368 // preclaim: tecFROZEN
4369 env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN));
4370 env.close();
4371
4372 env(fclear(issuer, asfGlobalFreeze));
4373 env.close();
4374
4375 auto const pseudoBroker = [&]() -> std::optional<Account> {
4376 if (auto brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
4377 BEAST_EXPECT(brokerSle))
4378 {
4379 return Account{"pseudo", brokerSle->at(sfAccount)};
4380 }
4381
4382 return std::nullopt;
4383 }();
4384 if (!pseudoBroker)
4385 return;
4386
4387 // Lender and pseudoaccount must both be frozen
4388 env(trust(issuer, lender["IOU"](1'000), lender, tfSetFreeze | tfSetDeepFreeze));
4389 env(trust(
4390 issuer, (*pseudoBroker)["IOU"](1'000), *pseudoBroker, tfSetFreeze | tfSetDeepFreeze));
4391 env.close();
4392
4393 // preclaim: tecFROZEN due to deep frozen
4394 env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecFROZEN));
4395 env.close();
4396
4397 // Only one needs to be unfrozen
4398 env(trust(issuer, lender["IOU"](1'000), tfClearFreeze | tfClearDeepFreeze));
4399 env.close();
4400
4401 // The payment is late by this point
4402 env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecEXPIRED));
4403 env.close();
4404 env(pay(borrower, loanKeylet.key, debtMaximumRequest, tfLoanLatePayment));
4405 env.close();
4406
4407 // preclaim: tecKILLED
4408 // note that tecKILLED in loanMakePayment()
4409 // doesn't happen because of the preclaim check.
4410 env(pay(borrower, loanKeylet.key, debtMaximumRequest), Ter(tecKILLED));
4411 }
4412
4413 void
4415 {
4416 testcase("Invalid LoanSet");
4417 using namespace jtx;
4418 using namespace loan;
4419 Account const lender{"lender"};
4420 Account const issuer{"issuer"};
4421 Account const borrower{"borrower"};
4422 auto const iou = issuer["IOU"];
4423
4424 auto testWrapper = [&](auto&& test) {
4425 Env env(*this);
4426 env.fund(XRP(1'000), lender, issuer, borrower);
4427 env(trust(lender, iou(10'000'000)));
4428 env(pay(issuer, lender, iou(5'000'000)));
4429 BrokerInfo const brokerInfo{createVaultAndBroker(env, issuer["IOU"], lender)};
4430
4431 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4432 Number const debtMaximumRequest = brokerInfo.asset(1'000).value();
4433 test(env, brokerInfo, loanSetFee, debtMaximumRequest);
4434 };
4435
4436 // preflight:
4437 testWrapper([&](Env& env,
4438 BrokerInfo const& brokerInfo,
4439 jtx::Fee const& loanSetFee,
4440 Number const& debtMaximumRequest) {
4441 // first temBAD_SIGNER: TODO
4442 // invalid grace period
4443 {
4444 // zero grace period
4445 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4446 Sig(sfCounterpartySignature, lender),
4447 kGracePeriod(0),
4448 loanSetFee,
4449 Ter(temINVALID));
4450
4451 // grace period less than default minimum
4452 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4453 Sig(sfCounterpartySignature, lender),
4454 kGracePeriod(LoanSet::kDefaultGracePeriod - 1),
4455 loanSetFee,
4456 Ter(temINVALID));
4457
4458 // grace period greater than payment interval
4459 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4460 Sig(sfCounterpartySignature, lender),
4461 kPaymentInterval(120),
4462 kGracePeriod(121),
4463 loanSetFee,
4464 Ter(temINVALID));
4465 }
4466 // empty/zero broker ID
4467 {
4468 auto jv = set(borrower, uint256{}, debtMaximumRequest);
4469
4470 auto testZeroBrokerID = [&](std::string const& id, std::uint32_t flags = 0) {
4471 // empty broker ID
4472 jv[sfLoanBrokerID] = id;
4473 env(jv,
4474 Sig(sfCounterpartySignature, lender),
4475 loanSetFee,
4476 Txflags(flags),
4477 Ter(temINVALID));
4478 };
4479 // empty broker ID
4480 testZeroBrokerID(std::string(""));
4481 // zero broker ID
4482 // needs a flag to distinguish the parsed STTx from the prior
4483 // test
4484 testZeroBrokerID(to_string(uint256{}), tfFullyCanonicalSig);
4485 }
4486
4487 // preflightCheckSigningKey() failure:
4488 // can it happen? the signature is checked before transactor
4489 // executes
4490
4491 JTx const tx = env.jt(
4492 set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4493 Sig(sfCounterpartySignature, lender),
4494 loanSetFee);
4495 STTx local = *(tx.stx);
4496 auto counterpartySig = local.getFieldObject(sfCounterpartySignature);
4497 auto badPubKey = counterpartySig.getFieldVL(sfSigningPubKey);
4498 badPubKey[20] ^= 0xAA;
4499 counterpartySig.setFieldVL(sfSigningPubKey, badPubKey);
4500 local.setFieldObject(sfCounterpartySignature, counterpartySig);
4501 json::Value jvResult;
4502 jvResult[jss::tx_blob] = strHex(local.getSerializer().slice());
4503 auto res = env.rpc("json", "submit", to_string(jvResult))["result"];
4504 BEAST_EXPECT(
4505 res[jss::error] == "invalidTransaction" &&
4506 res[jss::error_exception] ==
4507 "fails local checks: Counterparty: Invalid signature.");
4508 });
4509
4510 // preclaim:
4511 testWrapper([&](Env& env,
4512 BrokerInfo const& brokerInfo,
4513 jtx::Fee const& loanSetFee,
4514 Number const& debtMaximumRequest) {
4515 // canAddHoldingFailure (IOU only, if MPT doesn't have
4516 // MPTCanTransfer set, then can't create Vault/LoanBroker,
4517 // and LoanSet will fail with different error
4518 env(fclear(issuer, asfDefaultRipple));
4519 env.close();
4520 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4521 Sig(sfCounterpartySignature, lender),
4522 loanSetFee,
4523 Ter(terNO_RIPPLE));
4524 });
4525
4526 // doApply:
4527 testWrapper([&](Env& env,
4528 BrokerInfo const& brokerInfo,
4529 jtx::Fee const& loanSetFee,
4530 Number const& debtMaximumRequest) {
4531 auto const amt = env.balance(borrower) -
4532 env.current()->fees().accountReserve(env.ownerCount(borrower));
4533 env(pay(borrower, issuer, amt));
4534
4535 // tecINSUFFICIENT_RESERVE
4536 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4537 Sig(sfCounterpartySignature, lender),
4538 loanSetFee,
4540
4541 // addEmptyHolding failure
4542 env(pay(issuer, borrower, amt));
4543 env(fset(issuer, asfGlobalFreeze));
4544 env.close();
4545
4546 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4547 Sig(sfCounterpartySignature, lender),
4548 loanSetFee,
4549 Ter(tecFROZEN));
4550 });
4551 }
4552
4553 void
4555 {
4556 // (From FIND-006)
4557 testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount "
4558 "and MPT";
4559
4560 using namespace jtx;
4561 using namespace std::chrono_literals;
4562 Env env(*this, features);
4563
4564 Account const issuer{"issuer"};
4565 Account const lender{"lender"};
4566 Account const borrower{"borrower"};
4567
4568 env.fund(XRP(1'000'000), issuer, lender, borrower);
4569 env.close();
4570
4571 MPTTester mptt{env, issuer, kMptInitNoFund};
4572 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
4573 PrettyAsset const mptAsset = mptt.issuanceID();
4574 mptt.authorize({.account = lender});
4575 mptt.authorize({.account = borrower});
4576 env(pay(issuer, lender, mptAsset(2'000'000)));
4577 env(pay(issuer, borrower, mptAsset(1'000)));
4578 env.close();
4579
4580 BrokerInfo const broker{createVaultAndBroker(env, mptAsset, lender)};
4581
4582 using namespace loan;
4583
4584 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4585 Number const principalRequest{1, 3};
4586
4587 auto createJson = env.json(
4588 set(borrower, broker.brokerID, principalRequest),
4589 Fee(loanSetFee),
4590 Json(sfCounterpartySignature, json::ValueType::Object));
4591
4592 createJson["CloseInterestRate"] = 76671;
4593 createJson["ClosePaymentFee"] = "2061925410";
4594 createJson["GracePeriod"] = 434;
4595 createJson["InterestRate"] = 50302;
4596 createJson["LateInterestRate"] = 30322;
4597 createJson["LatePaymentFee"] = "294427911";
4598 createJson["LoanOriginationFee"] = "3250635102";
4599 createJson["LoanServiceFee"] = "9557386";
4600 createJson["OverpaymentFee"] = 51249;
4601 createJson["OverpaymentInterestRate"] = 14304;
4602 createJson["PaymentInterval"] = 434;
4603 createJson["PaymentTotal"] = "2891743748";
4604 createJson["PrincipalRequested"] = "8516.98";
4605
4606 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
4607
4608 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
4609 env(createJson, Ter(temINVALID));
4610 env.close();
4611 }
4612
4613 void
4615 {
4616 // From FIND-007
4617 testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease "
4618 "rounding good";
4619
4620 using namespace jtx;
4621 using namespace std::chrono_literals;
4622 using namespace Lending;
4623 Env env(*this, features);
4624
4625 Account const issuer{"issuer"};
4626 Account const lender{"lender"};
4627 Account const borrower{"borrower"};
4628
4629 env.fund(XRP(1'000'000), issuer, lender, borrower);
4630 env.close();
4631
4632 PrettyAsset const iouAsset = issuer[iouCurrency_];
4633 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4634 env(trustLenderTx);
4635 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4636 env(trustBorrowerTx);
4637 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4638 env(payLenderTx);
4639 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4640 env(payIssuerTx);
4641 env.close();
4642
4643 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
4644
4645 using namespace loan;
4646
4647 auto const baseFee = env.current()->fees().base;
4648 auto const loanSetFee = Fee(baseFee * 2);
4649 Number const principalRequest{1, 3};
4650
4651 auto createJson = env.json(
4652 set(borrower, broker.brokerID, principalRequest),
4653 Fee(loanSetFee),
4654 Json(sfCounterpartySignature, json::ValueType::Object));
4655
4656 createJson["ClosePaymentFee"] = "0";
4657 createJson["GracePeriod"] = 60;
4658 createJson["InterestRate"] = 24346;
4659 createJson["LateInterestRate"] = 65535;
4660 createJson["LatePaymentFee"] = "0";
4661 createJson["LoanOriginationFee"] = "218";
4662 createJson["LoanServiceFee"] = "0";
4663 createJson["PaymentInterval"] = 60;
4664 createJson["PaymentTotal"] = 5678;
4665 createJson["PrincipalRequested"] = "9924.81";
4666
4667 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
4668 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4669 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4670
4671 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
4672 env(createJson, Ter(tesSUCCESS));
4673 env.close();
4674
4675 auto const pseudoAcct = [&]() {
4676 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
4677 if (!BEAST_EXPECT(brokerSle))
4678 return Account{lender};
4679 auto const brokerPseudo = brokerSle->at(sfAccount);
4680 return Account("Broker pseudo-account", brokerPseudo);
4681 }();
4682
4683 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, keylet);
4684 auto const originalState = getCurrentState(env, broker, keylet);
4685 verifyLoanStatus(originalState);
4686
4687 Number const payment{3'269'349'176'470'588, -12};
4688 XRPAmount const payFee{
4689 baseFee *
4690 ((payment / originalState.periodicPayment) / kLoanPaymentsPerFeeIncrement + 1)};
4691 auto loanPayTx =
4692 env.json(pay(borrower, keylet.key, STAmount{broker.asset, payment}), Fee(payFee));
4693 BEAST_EXPECT(to_string(payment) == "3269.349176470588");
4694 env(loanPayTx, Ter(tesSUCCESS));
4695 env.close();
4696
4697 auto const newState = getCurrentState(env, broker, keylet);
4698 BEAST_EXPECT(
4699 isRounded(broker.asset, newState.managementFeeOutstanding, originalState.loanScale));
4700 BEAST_EXPECT(newState.managementFeeOutstanding < originalState.managementFeeOutstanding);
4701 BEAST_EXPECT(isRounded(broker.asset, newState.totalValue, originalState.loanScale));
4702 BEAST_EXPECT(
4703 isRounded(broker.asset, newState.principalOutstanding, originalState.loanScale));
4704 }
4705
4706 void
4708 {
4709 // From FIND-010
4710 testcase << "xrpl::loanComputePaymentParts : valid total interest";
4711
4712 using namespace jtx;
4713 using namespace std::chrono_literals;
4714 Env env(*this, features);
4715
4716 Account const issuer{"issuer"};
4717 Account const lender{"lender"};
4718 Account const borrower{"borrower"};
4719
4720 env.fund(XRP(1'000'000), issuer, lender, borrower);
4721 env.close();
4722
4723 PrettyAsset const iouAsset = issuer[iouCurrency_];
4724 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4725 env(trustLenderTx);
4726 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4727 env(trustBorrowerTx);
4728 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4729 env(payLenderTx);
4730 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4731 env(payIssuerTx);
4732 env.close();
4733
4734 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4735
4736 using namespace loan;
4737
4738 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4739 Number const principalRequest{1, 3};
4740
4741 auto createJson = env.json(
4742 set(borrower, broker.brokerID, principalRequest),
4743 Fee(loanSetFee),
4744 Json(sfCounterpartySignature, json::ValueType::Object));
4745
4746 createJson["CloseInterestRate"] = 47299;
4747 createJson["ClosePaymentFee"] = "3985819770";
4748 createJson["InterestRate"] = 92;
4749 createJson["LatePaymentFee"] = "3866894865";
4750 createJson["LoanOriginationFee"] = "0";
4751 createJson["LoanServiceFee"] = "2348810240";
4752 createJson["OverpaymentFee"] = 58545;
4753 createJson["PaymentInterval"] = 60;
4754 createJson["PaymentTotal"] = 1;
4755 createJson["PrincipalRequested"] = "0.000763058";
4756
4757 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
4758 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4759 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4760
4761 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
4762 env(createJson);
4763 env.close();
4764
4765 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4766 loanPayTx["Amount"]["value"] = "0.000281284125490196";
4767 env(loanPayTx, Ter(tecINSUFFICIENT_PAYMENT));
4768 env.close();
4769 }
4770
4771 void
4773 {
4774 bool const feeCapped = features[fixCleanup3_1_3];
4775
4776 // From FIND-005
4777 testcase << "DoS LoanPay: fee calculation " << (feeCapped ? "capped" : "uncapped");
4778
4779 using namespace jtx;
4780 using namespace std::chrono_literals;
4781 using namespace Lending;
4782 Env env(*this, features);
4783
4784 Account const issuer{"issuer"};
4785 Account const lender{"lender"};
4786 Account const borrower{"borrower"};
4787
4788 env.fund(XRP(1'000'000), issuer, lender, borrower);
4789 env.close();
4790
4791 BEAST_EXPECT(feeCapped == env.current()->rules().enabled(fixCleanup3_1_3));
4792
4793 PrettyAsset const iouAsset = issuer[iouCurrency_];
4794 env(trust(lender, iouAsset(100'000'000)));
4795 env(trust(borrower, iouAsset(100'000'000)));
4796 env(pay(issuer, lender, iouAsset(10'000'000)));
4797 env(pay(issuer, borrower, iouAsset(1'000)));
4798 env.close();
4799
4800 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4801
4802 using namespace loan;
4803
4804 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4805 Number const principalRequest{3959'37, -2};
4806 auto const baseFee = env.current()->fees().base;
4807
4808 auto const createJson = env.json(
4809 set(borrower, broker.brokerID, principalRequest),
4810 Fee(loanSetFee),
4811 Json(sfCounterpartySignature, json::ValueType::Object),
4812 kClosePaymentFee(0),
4813 kGracePeriod(60),
4814 kInterestRate(TenthBips32(20930)),
4815 kLateInterestRate(TenthBips32(77049)),
4816 kLatePaymentFee(0),
4817 kLoanServiceFee(0),
4818 kOverpaymentFee(TenthBips32(7)),
4819 kOverpaymentInterestRate(TenthBips32(66653)),
4820 kPaymentInterval(60),
4821 kPaymentTotal(3239184));
4822
4823 // There are enough payments due on this loan that it only needs to be
4824 // created once, and can be paid on multiple times. Just don't create a
4825 // gazillion test cases.
4826 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
4827 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4828 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4829
4830 env(createJson, Sig(sfCounterpartySignature, lender));
4831 env.close();
4832
4833 auto const roundedPayment = [&]() {
4834 auto const stateBefore = getCurrentState(env, broker, keylet);
4835 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
4836 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
4837
4838 return roundToAsset(
4839 iouAsset,
4840 stateBefore.periodicPayment,
4841 stateBefore.loanScale,
4843 }();
4844
4845 auto test = [&](int const payFactor,
4846 int const feeFactor,
4847 TER const expectedTer = tesSUCCESS) {
4848 auto const stateBefore = getCurrentState(env, broker, keylet);
4849 BEAST_EXPECT(stateBefore.paymentRemaining <= 3239184);
4850 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
4851
4852 Number const amount = roundedPayment * payFactor;
4853 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, amount}));
4854 XRPAmount const payFee{baseFee * feeFactor};
4855 env(loanPayTx, Ter(expectedTer), Fee(payFee));
4856 env.close();
4857 auto const expectedChange = isTesSuccess(expectedTer)
4858 ? std::min(kLoanMaximumPaymentsPerTransaction, payFactor)
4859 : 0;
4860
4861 auto const stateAfter = getCurrentState(env, broker, keylet);
4862 BEAST_EXPECT(
4863 stateAfter.paymentRemaining == stateBefore.paymentRemaining - expectedChange);
4864 };
4865
4866 static constexpr std::int64_t kMaxFeeIncrements =
4867 kLoanMaximumPaymentsPerTransaction / kLoanPaymentsPerFeeIncrement;
4868
4869 TER const failWithoutFix = feeCapped ? (TER)tesSUCCESS : (TER)telINSUF_FEE_P;
4870
4871 // * Amount well above threshold -> capped fee
4872 // The original test case - way over the limit - more fee is always ok
4873 test(1819878, 363976);
4874 // The capped fee is only sufficient if the amendment is enabled.
4875 test(1819878, kMaxFeeIncrements, failWithoutFix);
4876
4877 // * Amount exactly at threshold -> capped fee
4878 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements);
4879 // More fee is always ok
4880 test(kLoanMaximumPaymentsPerTransaction, kMaxFeeIncrements + 10);
4881
4882 // * Amount below threshold -> normal calculation
4883 test(1, 1);
4884 test(kLoanPaymentsPerFeeIncrement * 2, 2);
4885 test(0, 0, temBAD_AMOUNT);
4886 test(0, 1, temBAD_AMOUNT);
4887 // Fee difference rounds evenly
4888 test(
4889 kLoanMaximumPaymentsPerTransaction - 10,
4890 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) - 1,
4892 test(
4893 kLoanMaximumPaymentsPerTransaction - 10,
4894 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement));
4895 // More fee is always ok
4896 test(
4897 kLoanMaximumPaymentsPerTransaction - 10,
4898 ((kLoanMaximumPaymentsPerTransaction - 10) / kLoanPaymentsPerFeeIncrement) + 3);
4899 // Fee rounds up
4900 for (int under = 1; under < kLoanPaymentsPerFeeIncrement; ++under)
4901 {
4902 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements - 1, telINSUF_FEE_P);
4903 test(kLoanMaximumPaymentsPerTransaction - under, kMaxFeeIncrements);
4904 }
4905 // Only when you get one less fee increment can you pay less
4906 test(
4907 kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement,
4908 kMaxFeeIncrements - 1);
4909 // And again, more fee is always ok.
4910 test(kLoanMaximumPaymentsPerTransaction - kLoanPaymentsPerFeeIncrement, kMaxFeeIncrements);
4911 }
4912
4913 void
4915 {
4916 // From FIND-009
4917 testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid "
4918 "rounded";
4919
4920 using namespace jtx;
4921 using namespace std::chrono_literals;
4922 using namespace Lending;
4923 Env env(*this, features);
4924
4925 Account const issuer{"issuer"};
4926 Account const lender{"lender"};
4927 Account const borrower{"borrower"};
4928
4929 env.fund(XRP(1'000'000), issuer, lender, borrower);
4930 env.close();
4931
4932 PrettyAsset const iouAsset = issuer[iouCurrency_];
4933 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
4934 env(trustLenderTx);
4935 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
4936 env(trustBorrowerTx);
4937 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4938 env(payLenderTx);
4939 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4940 env(payIssuerTx);
4941 env.close();
4942
4943 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
4944
4945 using namespace loan;
4946
4947 auto const loanSetFee = Fee(env.current()->fees().base * 2);
4948 Number const principalRequest{1, 3};
4949
4950 auto createJson = env.json(
4951 set(borrower, broker.brokerID, principalRequest),
4952 Fee(loanSetFee),
4953 Json(sfCounterpartySignature, json::ValueType::Object));
4954
4955 createJson["ClosePaymentFee"] = "0";
4956 createJson["InterestRate"] = 24346;
4957 createJson["LateInterestRate"] = 65535;
4958 createJson["LatePaymentFee"] = "0";
4959 createJson["LoanOriginationFee"] = "218";
4960 createJson["LoanServiceFee"] = "0";
4961 createJson["PaymentInterval"] = 60;
4962 createJson["PaymentTotal"] = 5678;
4963 createJson["PrincipalRequested"] = "9924.81";
4964
4965 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
4966 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4967 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
4968
4969 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
4970 env(createJson, Ter(tesSUCCESS));
4971 env.close();
4972
4973 auto const baseFee = env.current()->fees().base;
4974
4975 auto const stateBefore = getCurrentState(env, broker, keylet);
4976
4977 {
4978 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4979 Number const amount{3074'745'058'823'529, -12};
4980 BEAST_EXPECT(to_string(amount) == "3074.745058823529");
4981 XRPAmount const payFee{
4982 baseFee *
4983 (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)};
4984 loanPayTx["Amount"]["value"] = to_string(amount);
4985 env(loanPayTx, Fee(payFee), Ter(tesSUCCESS));
4986 env.close();
4987 }
4988
4989 {
4990 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
4991 Number const amount{6732'118'170'944'051, -12};
4992 BEAST_EXPECT(to_string(amount) == "6732.118170944051");
4993 XRPAmount const payFee{
4994 baseFee *
4995 (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)};
4996 loanPayTx["Amount"]["value"] = to_string(amount);
4997 env(loanPayTx, Fee(payFee), Ter(tesSUCCESS));
4998 env.close();
4999 }
5000
5001 auto const stateAfter = getCurrentState(env, broker, keylet);
5002 // Total interest outstanding is non-negative
5003 BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding);
5004 // Principal paid is non-negative
5005 BEAST_EXPECT(stateBefore.principalOutstanding >= stateAfter.principalOutstanding);
5006 // Total value change is non-negative
5007 BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue);
5008 // Value delta is larger or same as principal delta (meaning
5009 // non-negative interest paid)
5010 BEAST_EXPECT(
5011 (stateBefore.totalValue - stateAfter.totalValue) >=
5012 (stateBefore.principalOutstanding - stateAfter.principalOutstanding));
5013 }
5014
5015 void
5017 {
5018 // From FIND-008
5019 testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded";
5020
5021 using namespace jtx;
5022 using namespace std::chrono_literals;
5023 using namespace Lending;
5024 Env env(*this, features);
5025
5026 Account const issuer{"issuer"};
5027 Account const lender{"lender"};
5028 Account const borrower{"borrower"};
5029
5030 env.fund(XRP(1'000'000), issuer, lender, borrower);
5031 env.close();
5032
5033 PrettyAsset const iouAsset = issuer[iouCurrency_];
5034 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
5035 env(trustLenderTx);
5036 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
5037 env(trustBorrowerTx);
5038 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
5039 env(payLenderTx);
5040 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
5041 env(payIssuerTx);
5042 env.close();
5043
5044 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender)};
5045 {
5046 auto const coverDepositValue = broker.asset(broker.params.coverDeposit * 10).value();
5047 env(loanBroker::coverDeposit(lender, broker.brokerID, coverDepositValue));
5048 env.close();
5049 }
5050
5051 using namespace loan;
5052
5053 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5054 Number const principalRequest{1, 3};
5055
5056 auto createJson = env.json(
5057 set(borrower, broker.brokerID, principalRequest),
5058 Fee(loanSetFee),
5059 Json(sfCounterpartySignature, json::ValueType::Object));
5060
5061 createJson["ClosePaymentFee"] = "0";
5062 createJson["InterestRate"] = 12833;
5063 createJson["LateInterestRate"] = 77048;
5064 createJson["LatePaymentFee"] = "0";
5065 createJson["LoanOriginationFee"] = "218";
5066 createJson["LoanServiceFee"] = "0";
5067 createJson["PaymentInterval"] = 752;
5068 createJson["PaymentTotal"] = 5678;
5069 createJson["PrincipalRequested"] = "9924.81";
5070
5071 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
5072 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5073 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5074
5075 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
5076 env(createJson, Ter(tesSUCCESS));
5077 env.close();
5078
5079 auto const baseFee = env.current()->fees().base;
5080
5081 auto const stateBefore = getCurrentState(env, broker, keylet);
5082 BEAST_EXPECT(stateBefore.paymentRemaining == 5678);
5083 BEAST_EXPECT(stateBefore.paymentRemaining > kLoanMaximumPaymentsPerTransaction);
5084
5085 auto loanPayTx = env.json(pay(borrower, keylet.key, STAmount{broker.asset, Number{}}));
5086 Number const amount{9924'81, -2};
5087 BEAST_EXPECT(to_string(amount) == "9924.81");
5088 XRPAmount const payFee{
5089 baseFee * (amount / stateBefore.periodicPayment / kLoanPaymentsPerFeeIncrement + 1)};
5090 loanPayTx["Amount"]["value"] = to_string(amount);
5091 env(loanPayTx, Fee(payFee), Ter(tesSUCCESS));
5092 env.close();
5093
5094 auto const stateAfter = getCurrentState(env, broker, keylet);
5095 BEAST_EXPECT(
5096 stateAfter.paymentRemaining ==
5097 stateBefore.paymentRemaining - kLoanMaximumPaymentsPerTransaction);
5098 }
5099
5100 void
5102 {
5103 // For FIND-013
5104 testcase << "Prevent nextPaymentDueDate overflow";
5105
5106 using namespace jtx;
5107 using namespace std::chrono_literals;
5108 using namespace Lending;
5109 Env env{*this, features};
5110
5111 Account const issuer{"issuer"};
5112 Account const lender{"lender"};
5113 Account const borrower{"borrower"};
5114
5115 env.fund(XRP(1'000'000), issuer, lender, borrower);
5116 env.close();
5117
5118 PrettyAsset const iouAsset = issuer[iouCurrency_];
5119 auto trustLenderTx = env.json(trust(lender, iouAsset(1'000'000'000)));
5120 env(trustLenderTx);
5121 auto trustBorrowerTx = env.json(trust(borrower, iouAsset(1'000'000'000)));
5122 env(trustBorrowerTx);
5123 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
5124 env(payLenderTx);
5125 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
5126 env(payIssuerTx);
5127 env.close();
5128
5129 BrokerParameters const brokerParams{.debtMax = Number{0}, .coverRateMin = TenthBips32{1}};
5130 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
5131
5132 using namespace loan;
5133
5134 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5135
5136 using timeType = decltype(sfNextPaymentDueDate)::type::value_type;
5138 constexpr timeType kMaxTime = std::numeric_limits<timeType>::max();
5139 static_assert(kMaxTime == 4'294'967'295);
5140
5141 auto const baseJson = [&]() {
5142 auto createJson = env.json(
5143 set(borrower, broker.brokerID, Number{55524'81, -2}),
5144 Fee(loanSetFee),
5145 kClosePaymentFee(0),
5146 kGracePeriod(LoanSet::kDefaultGracePeriod),
5147 kInterestRate(TenthBips32(12833)),
5148 kLateInterestRate(TenthBips32(77048)),
5149 kLatePaymentFee(0),
5150 kLoanOriginationFee(218),
5151 Json(sfCounterpartySignature, json::ValueType::Object));
5152
5153 createJson.removeMember(sfSequence.getJsonName());
5154
5155 return createJson;
5156 }();
5157
5158 auto const baseFee = env.current()->fees().base;
5159
5160 auto parentCloseTime = [&]() {
5161 return env.current()->parentCloseTime().time_since_epoch().count();
5162 };
5163 auto maxLoanTime = [&]() {
5164 auto const startDate = parentCloseTime();
5165
5166 BEAST_EXPECT(startDate >= 50);
5167
5168 return kMaxTime - startDate;
5169 };
5170
5171 {
5172 // straight-up overflow: interval
5173 auto const interval = maxLoanTime() + 1;
5174 auto const total = 1;
5175 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
5176
5177 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
5178 env.close();
5179 }
5180 {
5181 // straight-up overflow: total
5182 // min interval is 60
5183 auto const interval = 60;
5184 auto const total = maxLoanTime() + 1;
5185 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
5186
5187 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
5188 env.close();
5189 }
5190 {
5191 // straight-up overflow: grace period
5192 // min interval is 60
5193 auto const interval = maxLoanTime() + 1;
5194 auto const total = 1;
5195 auto const grace = interval;
5196 auto createJson = env.json(
5197 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
5198
5199 // The grace period can't be larger than the interval.
5200 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
5201 env.close();
5202 }
5203 {
5204 // Overflow with multiplication of a few large intervals
5205 auto const interval = 1'000'000'000;
5206 auto const total = 10;
5207 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
5208
5209 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
5210 env.close();
5211 }
5212 {
5213 // Overflow with multiplication of many small payments
5214 // min interval is 60
5215 auto const interval = 60;
5216 auto const total = 1'000'000'000;
5217 auto createJson = env.json(baseJson, kPaymentInterval(interval), kPaymentTotal(total));
5218
5219 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
5220 env.close();
5221 }
5222 {
5223 // Overflow with an absurdly large grace period
5224 // min interval is 60
5225 auto const total = 60;
5226 auto const interval = (maxLoanTime() - total) / total;
5227 auto const grace = interval;
5228 auto createJson = env.json(
5229 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
5230
5231 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tecKILLED));
5232 env.close();
5233 }
5234 {
5235 // Start date when the ledger is closed will be larger
5236 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
5237 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5238 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5239
5240 auto const grace = 100;
5241 auto const interval = maxLoanTime() - grace;
5242 auto const total = 1;
5243 auto createJson = env.json(
5244 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
5245
5246 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
5247 env.close();
5248
5249 // The transaction is killed in the closed ledger
5250 auto const meta = env.meta();
5251 if (BEAST_EXPECT(meta))
5252 {
5253 BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED);
5254 }
5255
5256 // If the transaction had succeeded, the loan would exist
5257 auto const loanSle = env.le(keylet);
5258 // but it doesn't
5259 BEAST_EXPECT(!loanSle);
5260 }
5261 {
5262 // Start date when the ledger is closed will be larger
5263 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
5264 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5265 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5266
5267 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
5268 auto const grace = 5'000;
5269 auto const interval = kMaxTime - closeStartDate - grace;
5270 auto const total = 1;
5271 auto createJson = env.json(
5272 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
5273
5274 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
5275 env.close();
5276
5277 // The transaction succeeds in the closed ledger
5278 auto const meta = env.meta();
5279 if (BEAST_EXPECT(meta))
5280 {
5281 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
5282 }
5283
5284 // This loan exists
5285 auto const afterState = getCurrentState(env, broker, keylet);
5286 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
5287 BEAST_EXPECT(afterState.previousPaymentDate == 0);
5288 BEAST_EXPECT(afterState.paymentRemaining == 1);
5289 }
5290
5291 {
5292 // Ensure the borrower has funds to pay back the loan
5293 env(pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2})));
5294
5295 // Start date when the ledger is closed will be larger
5296 auto const closeStartDate = ((parentCloseTime() / 10) + 1) * 10;
5297 auto const grace = 5'000;
5298 auto const maxLoanTime = kMaxTime - closeStartDate - grace;
5299 auto const total = [&]() {
5300 if (maxLoanTime % 5 == 0)
5301 return 5;
5302 if (maxLoanTime % 3 == 0)
5303 return 3;
5304 if (maxLoanTime % 2 == 0)
5305 return 2;
5306 return 0;
5307 }();
5308 if (!BEAST_EXPECT(total != 0))
5309 return;
5310
5311 auto const brokerState = env.le(keylet::loanBroker(broker.brokerID));
5312 // Intentionally shadow the outer values
5313 auto const loanSequence = brokerState->at(sfLoanSequence);
5314 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
5315
5316 auto const interval = maxLoanTime / total;
5317 auto createJson = env.json(
5318 baseJson, kPaymentInterval(interval), kPaymentTotal(total), kGracePeriod(grace));
5319
5320 env(createJson, Sig(sfCounterpartySignature, lender), Ter(tesSUCCESS));
5321 env.close();
5322
5323 // This loan exists
5324 auto const beforeState = getCurrentState(env, broker, keylet);
5325 BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval);
5326 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
5327 BEAST_EXPECT(beforeState.paymentRemaining == total);
5328 BEAST_EXPECT(beforeState.periodicPayment > 0);
5329
5330 // pay all but the last payment
5331 {
5333 Number const payment = beforeState.periodicPayment * (total - 1);
5334 XRPAmount const payFee{baseFee * ((total - 1) / kLoanPaymentsPerFeeIncrement + 1)};
5335 STAmount const paymentAmount =
5336 roundToScale(STAmount{broker.asset, payment}, beforeState.loanScale);
5337 auto loanPayTx = env.json(pay(borrower, keylet.key, paymentAmount), Fee(payFee));
5338 env(loanPayTx, Ter(tesSUCCESS));
5339 env.close();
5340 }
5341
5342 // The loan is on the last payment
5343 auto const afterState = getCurrentState(env, broker, keylet);
5344 BEAST_EXPECT(afterState.paymentRemaining == 1);
5345 BEAST_EXPECT(afterState.nextPaymentDate == kMaxTime - grace);
5346 BEAST_EXPECT(afterState.previousPaymentDate == kMaxTime - grace - interval);
5347 }
5348 }
5349
5350 void
5352 {
5353 testcase("Require Auth - Implicit Pseudo-account authorization");
5354 using namespace jtx;
5355 using namespace loan;
5356 Account const lender{"lender"};
5357 Account const issuer{"issuer"};
5358 Account const borrower{"borrower"};
5359 Env env(*this);
5360
5361 env.fund(XRP(100'000), issuer, lender, borrower);
5362 env.close();
5363
5364 auto asset = MPTTester({
5365 .env = env,
5366 .issuer = issuer,
5367 .holders = {lender, borrower},
5368 .flags = kMptDexFlags | tfMPTRequireAuth | tfMPTCanClawback | tfMPTCanLock,
5369 .authHolder = true,
5370 });
5371
5372 env(pay(issuer, lender, asset(5'000'000)));
5373 BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)};
5374
5375 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5376 STAmount const debtMaximumRequest = brokerInfo.asset(1'000).value();
5377
5378 auto forUnauthAuth = [&](auto&& doTx) {
5379 for (auto const flag : {tfMPTUnauthorize, 0u})
5380 {
5381 asset.authorize({.account = issuer, .holder = borrower, .flags = flag});
5382 env.close();
5383 doTx(flag == 0);
5384 env.close();
5385 }
5386 };
5387
5388 // Can't create a loan if the borrower is not authorized
5389 forUnauthAuth([&](bool authorized) {
5390 auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS);
5391 env(set(borrower, brokerInfo.brokerID, debtMaximumRequest),
5392 Sig(sfCounterpartySignature, lender),
5393 loanSetFee,
5394 err);
5395 });
5396
5397 static constexpr std::uint32_t kLoanSequence = 1;
5398 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, kLoanSequence);
5399
5400 // Can't loan pay if the borrower is not authorized
5401 forUnauthAuth([&](bool authorized) {
5402 auto const err = !authorized ? Ter(tecNO_AUTH) : Ter(tesSUCCESS);
5403 env(pay(borrower, loanKeylet.key, debtMaximumRequest), err);
5404 });
5405 }
5406
5407 void
5409 {
5410 testcase("Lending: CanTrade disabled has no impact");
5411 using namespace jtx;
5412 using namespace loan;
5413 using namespace loanBroker;
5414
5415 Env env(*this, all_);
5416
5417 Account const issuer{"issuer"};
5418 Account const lender{"lender"};
5419 Account const borrower{"borrower"};
5420
5421 env.fund(XRP(1'000'000), issuer, lender, borrower);
5422 env.close();
5423
5424 MPTTester mpt(
5425 {.env = env,
5426 .issuer = issuer,
5427 .holders = {lender, borrower},
5428 .flags = tfMPTCanTransfer | tfMPTCanLock,
5429 .mutableFlags = tmfMPTCanEnableCanTrade});
5430 PrettyAsset const asset = mpt.issuanceID();
5431 env(pay(issuer, lender, asset(10'000'000)));
5432 env(pay(issuer, borrower, asset(100'000)));
5433 env.close();
5434
5435 auto const broker = createVaultAndBroker(env, asset, lender);
5436
5437 // CanTrade is not set
5438 env(offer(lender, XRP(1), asset(10)), Ter{tecNO_PERMISSION});
5439 env.close();
5440
5441 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5442
5443 // New cover deposits still work.
5444 env(coverDeposit(lender, broker.brokerID, asset(100)));
5445 env.close();
5446
5447 // New loan issuance still works.
5448 env(loan::set(borrower, broker.brokerID, 1'000),
5449 Sig(sfCounterpartySignature, lender),
5450 loanSetFee);
5451 env.close();
5452 auto const loanKeylet = keylet::loan(broker.brokerID, 1);
5453 BEAST_EXPECT(env.le(loanKeylet));
5454
5455 // Repayment still works.
5456 env(pay(borrower, loanKeylet.key, asset(1'000)));
5457 env.close();
5458
5459 // Cover withdrawal still works.
5460 env(coverWithdraw(lender, broker.brokerID, asset(100)));
5461 env.close();
5462
5463 // Enable CanTrade and verify the DEX path is restored.
5464 mpt.set({.mutableFlags = tmfMPTSetCanTrade});
5465 env.close();
5466
5467 env(offer(lender, XRP(1), asset(10)));
5468 env.close();
5469 }
5470
5471#if LOAN_TODO
5472 void
5473 testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features)
5474 {
5475 testcase("LoanPay full payment skips late penalties");
5476 using namespace jtx;
5477 using namespace loan;
5478 using namespace std::chrono_literals;
5479
5480 Env env(*this, features);
5481
5482 Account const issuer{"issuer"};
5483 Account const lender{"lender"};
5484 Account const borrower{"borrower"};
5485
5486 env.fund(XRP(1'000'000), issuer, lender, borrower);
5487 env.close();
5488
5489 PrettyAsset const asset = issuer[iouCurrency];
5490 env(trust(lender, asset(100'000'000)));
5491 env(trust(borrower, asset(100'000'000)));
5492 env(pay(issuer, lender, asset(50'000'000)));
5493 env(pay(issuer, borrower, asset(5'000'000)));
5494 env.close();
5495
5496 BrokerInfo broker{createVaultAndBroker(env, asset, lender)};
5497
5498 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5499
5500 auto const brokerPreLoan = env.le(keylet::loanBroker(broker.brokerID));
5501 if (BEAST_EXPECT(brokerPreLoan); !brokerPreLoan.has_value())
5502 return;
5503
5504 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
5505 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
5506
5507 Number const principal = asset(1'000).value();
5508 Number const serviceFee = asset(2).value();
5509 Number const lateFee = asset(5).value();
5510 Number const closeFee = asset(4).value();
5511
5512 env(set(borrower, broker.brokerID, principal),
5513 Sig(sfCounterpartySignature, lender),
5514 kLoanServiceFee(serviceFee),
5515 kLatePaymentFee(lateFee),
5516 kClosePaymentFee(closeFee),
5520 kPaymentTotal(12),
5521 kPaymentInterval(600),
5522 kGracePeriod(0),
5523 Fee(loanSetFee));
5524 env.close();
5525
5526 auto state1 = getCurrentState(env, broker, loanKeylet);
5527 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
5528 return;
5529
5530 using d = NetClock::duration;
5531 using tp = NetClock::time_point;
5532 auto const overdueClose = tp{d{state1.nextPaymentDate + state1.paymentInterval}};
5533 env.close(overdueClose);
5534
5535 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
5536 auto const loanSle = env.le(loanKeylet);
5537 if (!BEAST_EXPECT(brokerSle && loanSle))
5538 return;
5539
5540 auto state = getCurrentState(env, broker, loanKeylet);
5541
5542 TenthBips16 const managementFeeRate{brokerSle->at(sfManagementFeeRate)};
5543 TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)};
5544 TenthBips32 const lateInterestRateValue{loanSle->at(sfLateInterestRate)};
5545 TenthBips32 const closeInterestRateValue{loanSle->at(sfCloseInterestRate)};
5546
5547 Number const closePaymentFeeRounded =
5548 roundToAsset(broker.asset, loanSle->at(sfClosePaymentFee), state.loanScale);
5549 Number const latePaymentFeeRounded =
5550 roundToAsset(broker.asset, loanSle->at(sfLatePaymentFee), state.loanScale);
5551
5552 auto const roundedLoanState = constructLoanState(
5553 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
5554 Number const totalInterestOutstanding = roundedLoanState.interestDue;
5555
5556 auto const periodicRate = loanPeriodicRate(interestRateValue, state.paymentInterval);
5557 auto const rawLoanState = computeTheoreticalLoanState(
5558 env.current()->rules(),
5559 state.periodicPayment,
5560 periodicRate,
5561 state.paymentRemaining,
5562 managementFeeRate);
5563
5564 auto const parentCloseTime = env.current()->parentCloseTime();
5565 auto const startDateSeconds =
5566 static_cast<std::uint32_t>(state.startDate.time_since_epoch().count());
5567
5568 Number const fullPaymentInterest = computeFullPaymentInterest(
5569 rawLoanState.principalOutstanding,
5570 periodicRate,
5571 parentCloseTime,
5572 state.paymentInterval,
5573 state.previousPaymentDate,
5574 startDateSeconds,
5575 closeInterestRateValue);
5576
5577 Number const roundedFullInterestAmount =
5578 roundToAsset(broker.asset, fullPaymentInterest, state.loanScale);
5579 Number const roundedFullManagementFee = computeManagementFee(
5580 broker.asset, roundedFullInterestAmount, managementFeeRate, state.loanScale);
5581 Number const roundedFullInterest = roundedFullInterestAmount - roundedFullManagementFee;
5582
5583 Number const trackedValueDelta =
5584 state.principalOutstanding + totalInterestOutstanding + state.managementFeeOutstanding;
5585 Number const untrackedManagementFee =
5586 closePaymentFeeRounded + roundedFullManagementFee - state.managementFeeOutstanding;
5587 Number const untrackedInterest = roundedFullInterest - totalInterestOutstanding;
5588
5589 Number const baseFullDue = trackedValueDelta + untrackedInterest + untrackedManagementFee;
5590 BEAST_EXPECT(baseFullDue == roundToAsset(broker.asset, baseFullDue, state.loanScale));
5591
5592 auto const overdueSeconds =
5593 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
5594 if (!BEAST_EXPECT(overdueSeconds > 0))
5595 return;
5596
5597 Number const overdueRate = loanPeriodicRate(lateInterestRateValue, overdueSeconds);
5598 Number const lateInterestRaw = state.principalOutstanding * overdueRate;
5599 Number const lateInterestRounded =
5600 roundToAsset(broker.asset, lateInterestRaw, state.loanScale);
5601 Number const lateManagementFeeRounded = computeManagementFee(
5602 broker.asset, lateInterestRounded, managementFeeRate, state.loanScale);
5603 Number const penaltyDue =
5604 lateInterestRounded + lateManagementFeeRounded + latePaymentFeeRounded;
5605 BEAST_EXPECT(penaltyDue > Number{});
5606
5607 auto const balanceBefore = env.balance(borrower, broker.asset).number();
5608
5609 STAmount const paymentAmount{broker.asset.raw(), baseFullDue};
5610 env(pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
5611 env.close();
5612
5613 if (auto const meta = env.meta(); BEAST_EXPECT(meta))
5614 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
5615
5616 auto const balanceAfter = env.balance(borrower, broker.asset).number();
5617 Number const actualPaid = balanceBefore - balanceAfter;
5618 BEAST_EXPECT(actualPaid == baseFullDue);
5619
5620 Number const expectedWithPenalty = baseFullDue + penaltyDue;
5621 BEAST_EXPECT(expectedWithPenalty > actualPaid);
5622 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
5623 }
5624
5625 void
5626 testLoanCoverMinimumRoundingExploit(FeatureBitset features)
5627 {
5628 auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) {
5629 testcase << "LoanBrokerCoverClawback drains cover via rounding"
5630 << " principalRequested=" << to_string(principalRequest);
5631
5632 using namespace jtx;
5633 using namespace loan;
5634 using namespace loanBroker;
5635
5636 Env env(*this, features);
5637
5638 Account const issuer{"issuer"};
5639 Account const lender{"lender"};
5640 Account const borrower{"borrower"};
5641
5642 env.fund(XRP(1'000'000'000), issuer, lender, borrower);
5643 env.close();
5644
5645 env(fset(issuer, asfAllowTrustLineClawback));
5646 env.close();
5647
5648 PrettyAsset const asset = issuer[iouCurrency];
5649 env(trust(lender, asset(2'000'0000)));
5650 env(trust(borrower, asset(2'000'0000)));
5651 env.close();
5652
5653 env(pay(issuer, lender, asset(2'000'0000)));
5654 env.close();
5655
5656 BrokerParameters brokerParams{.debtMax = 0, .coverRateMin = TenthBips32{10'000}};
5657 BrokerInfo broker{createVaultAndBroker(env, asset, lender, brokerParams)};
5658
5659 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5660 auto createTx = env.jt(
5661 set(borrower, broker.brokerID, principalRequest),
5662 Sig(sfCounterpartySignature, lender),
5663 loanSetFee,
5664 kPaymentInterval(600),
5665 kPaymentTotal(1),
5666 kGracePeriod(60));
5667 env(createTx);
5668 env.close();
5669
5670 auto const brokerBefore = env.le(keylet::loanBroker(broker.brokerID));
5671 BEAST_EXPECT(brokerBefore);
5672 if (!brokerBefore)
5673 return;
5674
5675 Number const debtOutstanding = brokerBefore->at(sfDebtTotal);
5676 Number const coverAvailableBefore = brokerBefore->at(sfCoverAvailable);
5677
5678 BEAST_EXPECT(debtOutstanding > Number{});
5679 BEAST_EXPECT(coverAvailableBefore > Number{});
5680
5681 log << "debt=" << to_string(debtOutstanding)
5682 << " cover_available=" << to_string(coverAvailableBefore);
5683
5684 env(coverClawback(issuer, 0), loanBrokerID(broker.brokerID));
5685 env.close();
5686
5687 auto const brokerAfter = env.le(keylet::loanBroker(broker.brokerID));
5688 BEAST_EXPECT(brokerAfter);
5689 if (!brokerAfter)
5690 return;
5691
5692 Number const debtAfter = brokerAfter->at(sfDebtTotal);
5693 // the debt has not changed
5694 BEAST_EXPECT(debtAfter == debtOutstanding);
5695
5696 Number const coverAvailableAfter = brokerAfter->at(sfCoverAvailable);
5697
5698 // since the cover rate min != 0, the cover available should not
5699 // be zero
5700 BEAST_EXPECT(coverAvailableAfter != Number{});
5701 };
5702
5703 // Call the lambda with different principal values
5704 testLoanCoverMinimumRoundingExploit(Number{1, -30}); // 1e-30 units
5705 testLoanCoverMinimumRoundingExploit(Number{1, -20}); // 1e-20 units
5706 testLoanCoverMinimumRoundingExploit(Number{1, -10}); // 1e-10 units
5707 testLoanCoverMinimumRoundingExploit(Number{1, 1}); // 1e-10 units
5708 }
5709#endif
5710
5711 void
5713 {
5714 // --- PoC Summary ----------------------------------------------------
5715 // Scenario: Borrower makes one periodic payment early (before next due)
5716 // so doPayment sets sfPreviousPaymentDueDate to the (future)
5717 // sfNextPaymentDueDate and advances sfNextPaymentDueDate by one
5718 // interval. Borrower then immediately performs a full-payment
5719 // (tfLoanFullPayment). Why it matters: Full-payment interest accrual
5720 // uses
5721 // delta = now - max(prevPaymentDate, startDate)
5722 // with an unsigned clock representation (uint32). If prevPaymentDate is
5723 // in the future, the subtraction underflows to a very large positive
5724 // number. This inflates roundedFullInterest and total full-close due,
5725 // and LoanPay applies the inflated valueChange to the vault
5726 // (sfAssetsTotal), increasing NAV.
5727 // --------------------------------------------------------------------
5728 testcase("PoC: Unsigned-underflow full-pay accrual after early periodic");
5729
5730 using namespace jtx;
5731 using namespace loan;
5732 using namespace std::chrono_literals;
5733
5734 Env env{*this, features};
5735
5736 Account const lender{"poc_lender4"};
5737 Account const borrower{"poc_borrower4"};
5738 env.fund(XRP(3'000'000), lender, borrower);
5739 env.close();
5740
5741 PrettyAsset const asset{xrpIssue(), 1'000'000};
5742 BrokerParameters const brokerParams{};
5743 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
5744
5745 // Create a 3-payment loan so full-payment path is enabled after 1
5746 // periodic payment.
5747 auto const loanSetFee = Fee(env.current()->fees().base * 2);
5748 Number const principalRequest = asset(1000).value();
5749 auto const originationFee = asset(0).value();
5750 auto const serviceFee = asset(1).value();
5751 auto const serviceFeePA = asset(1);
5752 auto const lateFee = asset(0).value();
5753 auto const closeFee = asset(0).value();
5754 auto const interest = percentageToTenthBips(12);
5755 auto const lateInterest = percentageToTenthBips(12) / 10;
5756 auto const closeInterest = percentageToTenthBips(12) / 10;
5757 auto const overpaymentInterest = percentageToTenthBips(12) / 10;
5758 auto const total = 3u;
5759 auto const interval = 600u;
5760 auto const grace = 60u;
5761
5762 auto createJtx = env.jt(
5763 set(borrower, broker.brokerID, principalRequest, 0),
5764 Sig(sfCounterpartySignature, lender),
5765 kLoanOriginationFee(originationFee),
5766 kLoanServiceFee(serviceFee),
5767 kLatePaymentFee(lateFee),
5768 kClosePaymentFee(closeFee),
5769 kOverpaymentFee(percentageToTenthBips(5) / 10),
5770 kInterestRate(interest),
5771 kLateInterestRate(lateInterest),
5772 kCloseInterestRate(closeInterest),
5773 kOverpaymentInterestRate(overpaymentInterest),
5774 kPaymentTotal(total),
5775 kPaymentInterval(interval),
5776 kGracePeriod(grace),
5777 Fee(loanSetFee));
5778
5779 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
5780 BEAST_EXPECT(brokerSle);
5781 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
5782 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
5783
5784 env(createJtx);
5785 env.close();
5786
5787 // Compute a regular periodic due and pay it early (before next due).
5788 auto state = getCurrentState(env, broker, loanKeylet);
5789 Number const periodicRate = loanPeriodicRate(state.interestRate, state.paymentInterval);
5790 auto const components = xrpl::detail::computePaymentComponents(
5791 env.current()->rules(),
5792 asset.raw(),
5793 state.loanScale,
5794 state.totalValue,
5795 state.principalOutstanding,
5796 state.managementFeeOutstanding,
5797 state.periodicPayment,
5798 periodicRate,
5799 state.paymentRemaining,
5800 brokerParams.managementFeeRate);
5801 STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()};
5802 // now < nextDue immediately after creation, so this is an early pay.
5803 env(pay(borrower, loanKeylet.key, regularDue));
5804 env.close();
5805
5806 // Immediately attempt a full payoff. Compute the exact full-payment
5807 // due to ensure the tx applies.
5808 auto after = getCurrentState(env, broker, loanKeylet);
5809 auto const loanSle = env.le(loanKeylet);
5810 BEAST_EXPECT(loanSle);
5811 auto const brokerSle2 = env.le(keylet::loanBroker(broker.brokerID));
5812 BEAST_EXPECT(brokerSle2);
5813
5814 auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) : Number{};
5815 auto const closeInterestRate =
5816 loanSle ? TenthBips32{loanSle->at(sfCloseInterestRate)} : TenthBips32{};
5817 auto const managementFeeRate =
5818 brokerSle2 ? TenthBips16{brokerSle2->at(sfManagementFeeRate)} : TenthBips16{};
5819
5820 Number const periodicRate2 = loanPeriodicRate(after.interestRate, after.paymentInterval);
5821 // Accrued + prepayment-penalty interest based on current periodic
5822 // schedule
5823 auto const fullPaymentInterest = computeFullPaymentInterest(
5825 env.current()->rules(),
5826 after.periodicPayment,
5827 periodicRate2,
5828 after.paymentRemaining),
5829 periodicRate2,
5830 env.current()->parentCloseTime(),
5831 after.paymentInterval,
5832 after.previousPaymentDate,
5833 static_cast<std::uint32_t>(after.startDate.time_since_epoch().count()),
5834 closeInterestRate);
5835
5836 // Round to asset scale and split interest/fee parts
5837 auto const roundedInterest =
5838 roundToAsset(asset.raw(), fullPaymentInterest, after.loanScale);
5839 Number const roundedFullMgmtFee =
5840 computeManagementFee(asset.raw(), roundedInterest, managementFeeRate, after.loanScale);
5841 Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee;
5842
5843 // Show both signed and unsigned deltas to highlight the underflow.
5844 auto const nowSecs =
5845 static_cast<std::uint32_t>(env.current()->parentCloseTime().time_since_epoch().count());
5846 auto const startSecs =
5847 static_cast<std::uint32_t>(after.startDate.time_since_epoch().count());
5848 auto const lastPaymentDate = std::max(after.previousPaymentDate, startSecs);
5849 auto const signedDelta =
5850 static_cast<std::int64_t>(nowSecs) - static_cast<std::int64_t>(lastPaymentDate);
5851 auto const unsignedDelta = static_cast<std::uint32_t>(nowSecs - lastPaymentDate);
5852 log << "PoC window: prev=" << after.previousPaymentDate << " start=" << startSecs
5853 << " now=" << nowSecs << " signedDelta=" << signedDelta
5854 << " unsignedDelta=" << unsignedDelta << std::endl;
5855
5856 // Reference (clamped) computation: emulate a non-negative accrual
5857 // window by clamping prevPaymentDate to 'now' for the full-pay path.
5858 auto const prevClamped = std::min(after.previousPaymentDate, nowSecs);
5859 auto const fullPaymentInterestClamped = computeFullPaymentInterest(
5861 env.current()->rules(),
5862 after.periodicPayment,
5863 periodicRate2,
5864 after.paymentRemaining),
5865 periodicRate2,
5866 env.current()->parentCloseTime(),
5867 after.paymentInterval,
5868 prevClamped,
5869 startSecs,
5870 closeInterestRate);
5871 auto const roundedInterestClamped =
5872 roundToAsset(asset.raw(), fullPaymentInterestClamped, after.loanScale);
5873 Number const roundedFullMgmtFeeClamped = computeManagementFee(
5874 asset.raw(), roundedInterestClamped, managementFeeRate, after.loanScale);
5875 Number const roundedFullInterestClamped =
5876 roundedInterestClamped - roundedFullMgmtFeeClamped;
5877 STAmount const fullDueClamped{
5878 asset,
5879 after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped +
5880 closePaymentFee};
5881
5882 // Collect vault NAV before closing payment
5883 auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) : uint256{};
5884 auto const vaultKey2 = keylet::vault(vaultId2);
5885 auto const vaultBefore = env.le(vaultKey2);
5886 BEAST_EXPECT(vaultBefore);
5887 Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) : Number{};
5888
5889 STAmount const fullDue{
5890 asset,
5891 after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee +
5892 closePaymentFee};
5893
5894 log << "PoC payoff: principalOutstanding=" << after.principalOutstanding
5895 << " roundedFullInterest=" << roundedFullInterest
5896 << " roundedFullMgmtFee=" << roundedFullMgmtFee << " closeFee=" << closePaymentFee
5897 << " fullDue=" << to_string(fullDue.getJson()) << std::endl;
5898 log << "PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped
5899 << " roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped
5900 << " fullDueClamped=" << to_string(fullDueClamped.getJson()) << std::endl;
5901
5902 env(pay(borrower, loanKeylet.key, fullDue), Txflags(tfLoanFullPayment));
5903 env.close();
5904
5905 // Sanity: underflow present (unsigned delta very large relative to
5906 // interval)
5907 BEAST_EXPECT(unsignedDelta > after.paymentInterval);
5908
5909 // Compare vault NAV before/after the full close
5910 auto const vaultAfter = env.le(vaultKey2);
5911 BEAST_EXPECT(vaultAfter);
5912 if (vaultAfter)
5913 {
5914 auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
5915 log << "PoC NAV: assetsTotalBefore=" << assetsTotalBefore
5916 << " assetsTotalAfter=" << assetsTotalAfter
5917 << " delta=" << (assetsTotalAfter - assetsTotalBefore) << std::endl;
5918
5919 // Value-based proof: underflowed window yields a payoff larger than
5920 // the clamped (non-underflow) reference.
5921 BEAST_EXPECT(fullDue == fullDueClamped);
5922 if (fullDue > fullDueClamped)
5923 log << "PoC delta: overcharge (fullDue > clamped)" << std::endl;
5924 }
5925
5926 // Loan should be paid off
5927 auto const finalLoan = env.le(loanKeylet);
5928 BEAST_EXPECT(finalLoan);
5929 if (finalLoan)
5930 {
5931 BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0);
5932 BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0);
5933 }
5934 }
5935
5936 void
5938 {
5939 testcase("Dust manipulation");
5940
5941 using namespace jtx;
5942 using namespace std::chrono_literals;
5943 Env env{*this, features};
5944
5945 // Setup: Create accounts
5946 Account const issuer{"issuer"};
5947 Account const lender{"lender"};
5948 Account const borrower{"borrower"};
5949 Account const victim{"victim"};
5950
5951 env.fund(XRP(1'000'000'00), issuer, lender, borrower, victim);
5952 env.close();
5953
5954 // Step 1: Create vault with IOU asset
5955 auto asset = issuer["USD"];
5956 env(trust(lender, asset(100000)));
5957 env(trust(borrower, asset(100000)));
5958 env(trust(victim, asset(100000)));
5959 env(pay(issuer, lender, asset(50000)));
5960 env(pay(issuer, borrower, asset(50000)));
5961 env(pay(issuer, victim, asset(50000)));
5962 env.close();
5963
5964 BrokerParameters const brokerParams{
5965 .vaultDeposit = 10000,
5966 .debtMax = Number{0},
5967 .coverRateMin = TenthBips32{1000},
5968 .coverRateLiquidation = TenthBips32{2500}};
5969
5970 auto broker = createVaultAndBroker(env, asset, lender, brokerParams);
5971
5972 auto const loanKeyletOpt = [&]() -> std::optional<Keylet> {
5973 auto const brokerSle = env.le(keylet::loanBroker(broker.brokerID));
5974 if (!BEAST_EXPECT(brokerSle))
5975 return std::nullopt;
5976
5977 // Broker has no loans
5978 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
5979
5980 // The loan keylet is based on the LoanSequence of the
5981 // _LOAN_BROKER_ object.
5982 auto const loanSequence = brokerSle->at(sfLoanSequence);
5983 return keylet::loan(broker.brokerID, loanSequence);
5984 }();
5985 if (!loanKeyletOpt)
5986 return;
5987
5988 auto const& vaultKeylet = broker.vaultKeylet();
5989
5990 {
5991 auto const vaultSle = env.le(vaultKeylet);
5992 Number const assetsTotal = vaultSle->at(sfAssetsTotal);
5993 Number const assetsAvail = vaultSle->at(sfAssetsAvailable);
5994
5995 log << "Before loan creation:" << std::endl;
5996 log << " AssetsTotal: " << assetsTotal << std::endl;
5997 log << " AssetsAvailable: " << assetsAvail << std::endl;
5998 log << " Difference: " << (assetsTotal - assetsAvail) << std::endl;
5999
6000 // before the loan the assets total and available should be equal
6001 BEAST_EXPECT(assetsAvail == assetsTotal);
6002 BEAST_EXPECT(assetsAvail == broker.asset(brokerParams.vaultDeposit).number());
6003 }
6004
6005 Keylet const& loanKeylet = *loanKeyletOpt;
6006
6007 LoanParameters const loanParams{
6008 .account = lender,
6009 .counter = borrower,
6010 .principalRequest = Number{100},
6011 .interest = TenthBips32{1922},
6012 .payTotal = 5816,
6013 .payInterval = 86400 * 6,
6014 .gracePd = 86400 * 5,
6015 };
6016
6017 env(loanParams(env, broker));
6018 env.close();
6019
6020 // Wait for loan to be late enough to default
6021 env.close(std::chrono::seconds(86400 * 40)); // 40 days
6022
6023 {
6024 auto const vaultSle = env.le(vaultKeylet);
6025 Number const assetsTotal = vaultSle->at(sfAssetsTotal);
6026 Number const assetsAvail = vaultSle->at(sfAssetsAvailable);
6027
6028 log << "After loan creation:" << std::endl;
6029 log << " AssetsTotal: " << assetsTotal << std::endl;
6030 log << " AssetsAvailable: " << assetsAvail << std::endl;
6031 log << " Difference: " << (assetsTotal - assetsAvail) << std::endl;
6032
6033 auto const loanSle = env.le(loanKeylet);
6034 if (!BEAST_EXPECT(loanSle))
6035 return;
6036 auto const state = constructRoundedLoanState(loanSle);
6037
6038 log << "Loan state:" << std::endl;
6039 log << " ValueOutstanding: " << state.valueOutstanding << std::endl;
6040 log << " PrincipalOutstanding: " << state.principalOutstanding << std::endl;
6041 log << " InterestOutstanding: " << state.interestOutstanding() << std::endl;
6042 log << " InterestDue: " << state.interestDue << std::endl;
6043 log << " FeeDue: " << state.managementFeeDue << std::endl;
6044
6045 // after loan creation the assets total and available should
6046 // reflect the value of the loan
6047 BEAST_EXPECT(assetsAvail < assetsTotal);
6048 BEAST_EXPECT(
6049 assetsAvail ==
6050 broker.asset(brokerParams.vaultDeposit - loanParams.principalRequest).number());
6051 BEAST_EXPECT(
6052 assetsTotal ==
6053 broker.asset(brokerParams.vaultDeposit + state.interestDue).number());
6054 }
6055
6056 // Step 7: Trigger default (dust adjustment will occur)
6057 env(jtx::loan::manage(lender, loanKeylet.key, tfLoanDefault));
6058 env.close();
6059
6060 // Step 8: Verify phantom assets created
6061 {
6062 auto const vaultSle2 = env.le(vaultKeylet);
6063 Number const assetsTotal2 = vaultSle2->at(sfAssetsTotal);
6064 Number const assetsAvail2 = vaultSle2->at(sfAssetsAvailable);
6065
6066 log << "After default:" << std::endl;
6067 log << " AssetsTotal: " << assetsTotal2 << std::endl;
6068 log << " AssetsAvailable: " << assetsAvail2 << std::endl;
6069 log << " Difference: " << (assetsTotal2 - assetsAvail2) << std::endl;
6070
6071 // after a default the assets total and available should be equal
6072 BEAST_EXPECT(assetsAvail2 == assetsTotal2);
6073 }
6074 }
6075
6076 void
6078 {
6079 using namespace jtx;
6080
6081 testcase("RIPD-3831");
6082
6083 Account const issuer("issuer");
6084 Account const lender("lender");
6085 Account const borrower("borrower");
6086
6087 BrokerParameters const brokerParams{
6088 .vaultDeposit = 100000,
6089 .debtMax = 0,
6090 .coverRateMin = TenthBips32{0},
6091 // .managementFeeRate = TenthBips16{5919},
6092 .coverRateLiquidation = TenthBips32{0}};
6093 LoanParameters const loanParams{
6094 .account = lender,
6095 .counter = borrower,
6096 .principalRequest = Number{200'000, -6},
6097 .lateFee = Number{200, -6},
6098 .interest = TenthBips32{50'000},
6099 .payTotal = 10,
6100 .payInterval = 150};
6101
6102 auto const assetType = AssetType::XRP;
6103
6104 Env env{*this, features};
6105
6106 auto loanResult =
6107 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6108
6109 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6110 return;
6111
6112 auto broker = std::get<BrokerInfo>(*loanResult);
6113 auto loanKeylet = std::get<Keylet>(*loanResult);
6114
6115 using tp = NetClock::time_point;
6116 using d = NetClock::duration;
6117
6118 auto state = getCurrentState(env, broker, loanKeylet);
6119 if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
6120 {
6121 env.close(tp{d{loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
6122 }
6123
6124 topUpBorrower(env, broker, issuer, borrower, state, loanParams.serviceFee);
6125
6126 using namespace jtx::loan;
6127
6128 auto jv = pay(borrower, loanKeylet.key, drops(XRPAmount(state.totalValue)));
6129
6130 {
6131 auto const submitParam = to_string(jv);
6132 auto const jr = env.rpc("submit", borrower.name(), submitParam);
6133
6134 BEAST_EXPECT(jr.isMember(jss::result));
6135 auto const jResult = jr[jss::result];
6136 }
6137
6138 env.close();
6139
6140 // Make sure the system keeps responding
6141 env(noop(borrower));
6142 env.close();
6143 env(noop(issuer));
6144 env.close();
6145 env(noop(lender));
6146 env.close();
6147 }
6148
6149 void
6151 {
6152 testcase("RIPD-3459 - LoanBroker incorrect debt total");
6153
6154 using namespace jtx;
6155
6156 Account const issuer("issuer");
6157 Account const lender("lender");
6158 Account const borrower("borrower");
6159
6160 BrokerParameters const brokerParams{
6161 .vaultDeposit = 200'000,
6162 .debtMax = 0,
6163 .coverRateMin = TenthBips32{0},
6164 .managementFeeRate = TenthBips16{500},
6165 .coverRateLiquidation = TenthBips32{0}};
6166 LoanParameters const loanParams{
6167 .account = lender,
6168 .counter = borrower,
6169 .principalRequest = Number{100'000, -4},
6170 .interest = TenthBips32{100'000},
6171 .payTotal = 10};
6172
6173 auto const assetType = AssetType::MPT;
6174
6175 Env env{*this, features};
6176
6177 auto loanResult =
6178 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6179
6180 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6181 return;
6182
6183 auto broker = std::get<BrokerInfo>(*loanResult);
6184 auto loanKeylet = std::get<Keylet>(*loanResult);
6185 auto pseudoAcct = std::get<Account>(*loanResult);
6186
6187 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6188
6189 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
6190 {
6191 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
6192 {
6193 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
6194 }
6195 }
6196
6198 env,
6199 broker,
6200 loanParams,
6201 loanKeylet,
6202 verifyLoanStatus,
6203 issuer,
6204 lender,
6205 borrower,
6206 PaymentParameters{.showStepBalances = true});
6207
6208 if (auto const brokerSle = env.le(broker.brokerKeylet()); BEAST_EXPECT(brokerSle))
6209 {
6210 if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
6211 {
6212 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == loanSle->at(sfTotalValueOutstanding));
6213 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::kZero);
6214 }
6215 }
6216 }
6217
6218 void
6220 {
6221 testcase("Crash with tfLoanOverpayment");
6222 using namespace jtx;
6223 using namespace loan;
6224 Account const lender{"lender"};
6225 Account const issuer{"issuer"};
6226 Account const borrower{"borrower"};
6227 Account const depositor{"depositor"};
6228 auto const txFee = Fee(XRP(100));
6229
6230 Env env(*this);
6231 Vault const vault(env);
6232
6233 env.fund(XRP(10'000), lender, issuer, borrower, depositor);
6234 env.close();
6235
6236 auto [tx, vaultKeyLet] = vault.create({.owner = lender, .asset = xrpIssue()});
6237 env(tx, txFee);
6238 env.close();
6239
6240 env(vault.deposit({.depositor = depositor, .id = vaultKeyLet.key, .amount = XRP(1'000)}),
6241 txFee);
6242 env.close();
6243
6244 auto const brokerKeyLet = keylet::loanBroker(lender.id(), env.seq(lender));
6245
6246 env(loanBroker::set(lender, vaultKeyLet.key), txFee);
6247 env.close();
6248
6249 // BrokerInfo brokerInfo{xrpIssue(), keylet, vaultKeyLet, {}};
6250
6251 STAmount const debtMaximumRequest = XRPAmount(200'000);
6252
6253 env(set(borrower, brokerKeyLet.key, debtMaximumRequest),
6254 Sig(sfCounterpartySignature, lender),
6255 kInterestRate(TenthBips32(50'000)),
6256 kPaymentTotal(2),
6257 kPaymentInterval(150),
6258 Txflags(tfLoanOverpayment),
6259 txFee);
6260 env.close();
6261
6262 std::uint32_t const loanSequence = 1;
6263 auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence);
6264
6265 if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
6266 {
6267 env(loan::pay(borrower, loanKeylet.key, XRPAmount(150'001)),
6268 Txflags(tfLoanOverpayment),
6269 txFee);
6270 env.close();
6271 }
6272 }
6273
6274 void
6276 {
6277 testcase("Minimum cover rounding allows undercoverage (XRP)");
6278
6279 using namespace jtx;
6280 using namespace loanBroker;
6281
6282 Env env{*this, features};
6283
6284 Account const lender{"lender"};
6285 Account const borrower{"borrower"};
6286
6287 env.fund(XRP(200'000), lender, borrower);
6288 env.close();
6289
6290 // Vault with XRP asset
6291 Vault const vault{env};
6292 auto [vaultCreate, vaultKeylet] = vault.create({.owner = lender, .asset = xrpIssue()});
6293 env(vaultCreate);
6294 env.close();
6295 BEAST_EXPECT(env.le(vaultKeylet));
6296
6297 // Seed the vault with XRP so it can fund the loan principal
6298 PrettyAsset const xrpAsset{xrpIssue(), 1};
6299
6300 BrokerParameters const brokerParams{
6301 .vaultDeposit = 1'000,
6302 .debtMax = Number{0},
6303 .coverRateMin = TenthBips32{10'000},
6304 .coverDeposit = 82,
6305 };
6306
6307 auto const brokerInfo = createVaultAndBroker(env, xrpAsset, lender, brokerParams);
6308 // Create a loan with principal 804 XRP and 0% interest (so
6309 // DebtTotal increases by exactly 804)
6310 env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()),
6312 Sig(sfCounterpartySignature, lender),
6313 Fee(env.current()->fees().base * 2));
6314 BEAST_EXPECT(env.ter() == tesSUCCESS);
6315 env.close();
6316
6317 // Verify DebtTotal is exactly 804
6318 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
6319 BEAST_EXPECT(brokerSle))
6320 {
6321 log << *brokerSle << std::endl;
6322 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804));
6323 }
6324
6325 // Attempt to withdraw 2 XRP to self, leaving 80 XRP CoverAvailable.
6326 // The minimum is 80.4 XRP, which rounds up to 81 XRP, so this fails.
6327 env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()),
6329 BEAST_EXPECT(env.ter() == tecINSUFFICIENT_FUNDS);
6330 env.close();
6331
6332 // Attempt to withdraw 1 XRP to self, leaving 81 XRP CoverAvailable.
6333 // because that leaves sufficient cover, this succeeds
6334 env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value()));
6335 BEAST_EXPECT(env.ter() == tesSUCCESS);
6336 env.close();
6337
6338 // Validate CoverAvailable == 80 XRP and DebtTotal remains 804
6339 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
6340 BEAST_EXPECT(brokerSle))
6341 {
6342 log << *brokerSle << std::endl;
6343 BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == xrpAsset(81).value());
6344 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == Number(804));
6345
6346 // Also demonstrate that the true minimum (804 * 10%) exceeds 80
6347 auto const theoreticalMin = tenthBipsOfValue(Number(804), TenthBips32(10'000));
6348 log << "Theoretical min cover: " << theoreticalMin << std::endl;
6349 BEAST_EXPECT(Number(804, -1) == theoreticalMin);
6350 }
6351 }
6352
6353 void
6355 {
6356 testcase("RIPD-3902 - 1 IOU loan payments");
6357
6358 using namespace jtx;
6359
6360 Account const issuer("issuer");
6361 Account const lender("lender");
6362 Account const borrower("borrower");
6363
6364 BrokerParameters const brokerParams{
6365 .vaultDeposit = 10,
6366 .debtMax = 0,
6367 .coverRateMin = TenthBips32{0},
6368 .managementFeeRate = TenthBips16{0},
6369 .coverRateLiquidation = TenthBips32{0}};
6370 LoanParameters const loanParams{
6371 .account = lender,
6372 .counter = borrower,
6373 .principalRequest = Number{1, 0},
6374 .interest = TenthBips32{100'000},
6375 .payTotal = 5,
6376 .payInterval = 150,
6377 .gracePd = 60};
6378
6379 auto const assetType = AssetType::IOU;
6380
6381 Env env{*this, features};
6382
6383 auto loanResult =
6384 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6385
6386 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6387 return;
6388
6389 auto broker = std::get<BrokerInfo>(*loanResult);
6390 auto loanKeylet = std::get<Keylet>(*loanResult);
6391 auto pseudoAcct = std::get<Account>(*loanResult);
6392
6393 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6394
6396 env,
6397 broker,
6398 loanParams,
6399 loanKeylet,
6400 verifyLoanStatus,
6401 issuer,
6402 lender,
6403 borrower,
6404 PaymentParameters{.showStepBalances = true});
6405 }
6406
6407 void
6409 {
6410 testcase("Test Borrower is Broker");
6411 using namespace jtx;
6412 using namespace loan;
6413 Account const broker{"broker"};
6414 Account const issuer{"issuer"};
6415 Account const borrower{"borrower"};
6416 Account const depositor{"depositor"};
6417
6418 auto testLoanAsset = [&](auto&& getMaxDebt, auto const& borrower) {
6419 Env env(*this);
6420 Vault const vault(env);
6421
6422 if (borrower == broker)
6423 {
6424 env.fund(XRP(10'000), broker, issuer, depositor);
6425 }
6426 else
6427 {
6428 env.fund(XRP(10'000), broker, borrower, issuer, depositor);
6429 }
6430 env.close();
6431
6432 auto const xrpFee = XRP(100);
6433 auto const txFee = Fee(xrpFee);
6434
6435 STAmount const debtMaximumRequest = getMaxDebt(env);
6436
6437 auto const& asset = debtMaximumRequest.asset();
6438 auto const initialVault = asset(debtMaximumRequest * 100);
6439
6440 auto [tx, vaultKeylet] = vault.create({.owner = broker, .asset = asset});
6441 env(tx, txFee);
6442 env.close();
6443
6444 env(vault.deposit(
6445 {.depositor = depositor, .id = vaultKeylet.key, .amount = initialVault}),
6446 txFee);
6447 env.close();
6448
6449 auto const brokerKeylet = keylet::loanBroker(broker.id(), env.seq(broker));
6450
6451 env(loanBroker::set(broker, vaultKeylet.key), txFee);
6452 env.close();
6453
6454 auto const serviceFee = 101;
6455
6456 env(set(broker, brokerKeylet.key, debtMaximumRequest),
6457 kCounterparty(borrower),
6458 Sig(sfCounterpartySignature, borrower),
6459 kLoanServiceFee(serviceFee),
6460 kPaymentTotal(10),
6461 txFee);
6462 env.close();
6463
6464 std::uint32_t const loanSequence = 1;
6465 auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence);
6466
6467 auto const brokerBalanceBefore = env.balance(broker, asset);
6468
6469 if (auto const loanSle = env.le(loanKeylet); env.test.BEAST_EXPECT(loanSle))
6470 {
6471 auto const payment = loanSle->at(sfPeriodicPayment);
6472 auto const totalPayment = payment + serviceFee;
6473 env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)), txFee);
6474 env.close();
6475 if (auto const vaultSle = env.le(vaultKeylet); BEAST_EXPECT(vaultSle))
6476 {
6477 auto const expected = [&]() {
6478 // The service fee is transferred to the broker if
6479 // a borrower is not the broker
6480 if (borrower != broker)
6481 return brokerBalanceBefore.number() + serviceFee;
6482 // Since a borrower is the broker, the payment is
6483 // transferred to the Vault from the broker but not
6484 // the service fee.
6485 // If the asset is XRP then the broker pays the txFee.
6486 if (asset.native())
6487 return brokerBalanceBefore.number() - payment - xrpFee.number();
6488 return brokerBalanceBefore.number() - payment;
6489 }();
6490 BEAST_EXPECT(env.balance(broker, asset).value() == asset(expected).value());
6491 }
6492 }
6493 };
6494 // Test when a borrower is the broker and is not to verify correct
6495 // service fee transfer in both cases.
6496 for (auto const& borrowerAcct : {broker, borrower})
6497 {
6498 testLoanAsset(
6499 [&](Env&) -> STAmount { return STAmount{XRPAmount{200'000}}; }, borrowerAcct);
6500 testLoanAsset(
6501 [&](Env& env) -> STAmount {
6502 auto const iou = issuer["USD"];
6503 env(trust(broker, iou(1'000'000'000)));
6504 env(trust(depositor, iou(1'000'000'000)));
6505 env(pay(issuer, broker, iou(100'000'000)));
6506 env(pay(issuer, depositor, iou(100'000'000)));
6507 env.close();
6508 return iou(200'000);
6509 },
6510 borrowerAcct);
6511 testLoanAsset(
6512 [&](Env& env) -> STAmount {
6513 MPTTester const mpt(
6514 {.env = env,
6515 .issuer = issuer,
6516 .holders = {broker, depositor},
6517 .pay = 100'000'000});
6518 return mpt(200'000);
6519 },
6520 borrowerAcct);
6521 }
6522 }
6523
6524 void
6526 {
6527 testcase("RIPD-4096 - Issuer as borrower");
6528
6529 using namespace jtx;
6530
6531 Account const issuer("issuer");
6532 Account const lender("lender");
6533
6534 BrokerParameters const brokerParams{
6535 .vaultDeposit = 100'000,
6536 .debtMax = 0,
6537 .coverRateMin = TenthBips32{0},
6538 .managementFeeRate = TenthBips16{0},
6539 .coverRateLiquidation = TenthBips32{0}};
6540 LoanParameters const loanParams{
6541 .account = lender, .counter = issuer, .principalRequest = Number{10000}};
6542
6543 auto const assetType = AssetType::IOU;
6544
6545 Env env{*this, features};
6546
6547 auto loanResult =
6548 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer);
6549
6550 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6551 return;
6552
6553 auto broker = std::get<BrokerInfo>(*loanResult);
6554 auto loanKeylet = std::get<Keylet>(*loanResult);
6555 auto pseudoAcct = std::get<Account>(*loanResult);
6556
6557 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6558
6560 env,
6561 broker,
6562 loanParams,
6563 loanKeylet,
6564 verifyLoanStatus,
6565 issuer,
6566 lender,
6567 issuer,
6568 PaymentParameters{.showStepBalances = true});
6569 }
6570
6571 void
6573 {
6574 testcase("RIPD-4125 - overpayment");
6575
6576 using namespace jtx;
6577
6578 Account const issuer("issuer");
6579 Account const lender("lender");
6580 Account const borrower("borrower");
6581
6582 BrokerParameters const brokerParams{
6583 .vaultDeposit = 100'000,
6584 .debtMax = 0,
6585 .coverRateMin = TenthBips32{0},
6586 .managementFeeRate = TenthBips16{0},
6587 .coverRateLiquidation = TenthBips32{0}};
6588 LoanParameters const loanParams{
6589 .account = lender,
6590 .counter = borrower,
6591 .principalRequest = Number{200000, -6},
6592 .interest = TenthBips32{50000},
6593 .payTotal = 3,
6594 .payInterval = 200,
6595 .gracePd = 60,
6596 .flags = tfLoanOverpayment,
6597 };
6598
6599 auto const assetType = AssetType::XRP;
6600
6601 Env env(*this, makeConfig(), all_, nullptr, beast::Severity::Warning);
6602
6603 auto loanResult =
6604 createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6605
6606 if (BEAST_EXPECT(loanResult); !loanResult.has_value())
6607 return;
6608
6609 auto broker = std::get<BrokerInfo>(*loanResult);
6610 auto loanKeylet = std::get<Keylet>(*loanResult);
6611 auto pseudoAcct = std::get<Account>(*loanResult);
6612
6613 VerifyLoanStatus const verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
6614
6615 auto const state = getCurrentState(env, broker, loanKeylet);
6616
6617 env(loan::pay(
6618 borrower,
6619 loanKeylet.key,
6620 STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1},
6621 tfLoanOverpayment));
6622 env.close();
6623
6624 PaymentParameters const paymentParams{
6625 .showStepBalances = false,
6626 .validateBalances = true,
6627 };
6628
6630 env,
6631 broker,
6632 loanParams,
6633 loanKeylet,
6634 verifyLoanStatus,
6635 issuer,
6636 lender,
6637 borrower,
6638 paymentParams);
6639 }
6640
6641 void
6643 {
6644 testcase("testOverpaymentManagementFee");
6645
6646 using namespace jtx;
6647 using namespace loan;
6648
6649 Env env{*this, features};
6650
6651 Account const lender{"lender"}, borrower{"borrower"};
6652
6653 env.fund(XRP(10'000'000), lender, borrower);
6654 env.close();
6655
6656 PrettyAsset const asset{xrpIssue(), 1000};
6657
6658 auto const result = createVaultAndBroker(
6659 env,
6660 asset,
6661 lender,
6662 {
6663 .vaultDeposit = asset(100'000).value(),
6664 .managementFeeRate = TenthBips16(10'000),
6665 });
6666
6667 auto const loanSetFee = Fee(env.current()->fees().base * 2);
6668
6669 auto const loanKeylet = keylet::loan(
6670 result.brokerKeylet().key, (env.le(result.brokerKeylet()))->at(sfLoanSequence));
6671 env(loan::set(
6672 borrower, result.brokerKeylet().key, asset(10'000).value(), tfLoanOverpayment),
6673 Sig(sfCounterpartySignature, lender),
6674 loan::kPaymentInterval(86400 * 30),
6677 loanSetFee);
6678
6679 // From calculator
6680 auto const expectedOverpaymentManagementFee = Number{33333, 0};
6681 auto const loanBrokerBalanceBefore = env.balance(lender);
6682
6683 auto const loanPayFee = Fee(env.current()->fees().base * 2);
6684 env(pay(borrower, loanKeylet.key, asset(5'000).value(), tfLoanOverpayment), loanPayFee);
6685 env.close();
6686
6687 BEAST_EXPECTS(
6688 env.balance(lender) - loanBrokerBalanceBefore == expectedOverpaymentManagementFee,
6689 "overpayment management fee missmatch; expected:" +
6690 to_string(expectedOverpaymentManagementFee) +
6691 " got: " + to_string(env.balance(lender) - loanBrokerBalanceBefore));
6692 }
6693
6694 void
6696 {
6697 testcase << "LoanPay Broker Owner Missing Trustline (PoC)";
6698 using namespace jtx;
6699 using namespace loan;
6700 Account const issuer("issuer");
6701 Account const borrower("borrower");
6702 Account const broker("broker");
6703 auto const iou = issuer["IOU"];
6704 Env env(*this, features);
6705 env.fund(XRP(20'000), issuer, broker, borrower);
6706 env.close();
6707 // Set up trustlines and fund accounts
6708 env(trust(broker, iou(20'000'000)));
6709 env(trust(borrower, iou(20'000'000)));
6710 env(pay(issuer, broker, iou(10'000'000)));
6711 env(pay(issuer, borrower, iou(1'000)));
6712 env.close();
6713 // Create vault and broker
6714 auto const brokerInfo = createVaultAndBroker(env, iou, broker);
6715 // Create a loan first (this creates debt)
6716 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
6717 env(set(borrower, brokerInfo.brokerID, 10'000),
6718 Sig(sfCounterpartySignature, broker),
6719 kLoanServiceFee(iou(100).value()),
6720 kPaymentInterval(100),
6721 Fee(XRP(100)));
6722 env.close();
6723 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
6724 // We need coverAvailable >= (debtTotal * coverRateMinimum)
6725 // Deposit enough cover to ensure the fee goes to broker owner
6726 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
6727 // at least 1,000 cover. Default cover is 1,000, so we add more to be
6728 // safe.
6729 auto const additionalCover = iou(50'000).value();
6730 env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{iou, additionalCover}));
6731 env.close();
6732 // Verify broker owner has a trustline
6733 auto const brokerTrustline = keylet::trustLine(broker, iou);
6734 BEAST_EXPECT(env.le(brokerTrustline) != nullptr);
6735 // Broker owner deletes their trustline
6736 // First, pay any positive balance to issuer to zero it out
6737 auto const brokerBalance = env.balance(broker, iou);
6738 env(pay(broker, issuer, brokerBalance));
6739 env.close();
6740 // Remove the trustline by setting limit to 0
6741 env(trust(broker, iou(0)));
6742 env.close();
6743 // Verify trustline is deleted
6744 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
6745 // Now borrower tries to make a payment
6746 // We should get a tesSUCCESS instead of a tecNO_LINE.
6747 env(pay(borrower, keylet.key, iou(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
6748 env.close();
6749 // Verify trustline is still deleted
6750 BEAST_EXPECT(env.le(brokerTrustline) == nullptr);
6751 // Verify the service fee went to the broker pseudo-account
6752 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
6753 BEAST_EXPECT(brokerSle))
6754 {
6755 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
6756 auto const balance = env.balance(pseudo, iou);
6757 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
6758 BEAST_EXPECTS(balance == iou(51'100), to_string(json::Value(balance)));
6759 }
6760 }
6761
6762 void
6764 {
6765 testcase << "LoanPay Broker Owner MPT unauthorized";
6766 using namespace jtx;
6767 using namespace loan;
6768
6769 Account const issuer("issuer");
6770 Account const borrower("borrower");
6771 Account const broker("broker");
6772
6773 Env env{*this, features};
6774 env.fund(XRP(20'000), issuer, broker, borrower);
6775 env.close();
6776
6777 MPTTester mptt{env, issuer, kMptInitNoFund};
6778 mptt.create({.flags = tfMPTCanClawback | tfMPTCanTransfer | tfMPTCanLock});
6779
6780 PrettyAsset const mpt{mptt.issuanceID()};
6781
6782 // Authorize broker and borrower
6783 mptt.authorize({.account = broker});
6784 mptt.authorize({.account = borrower});
6785
6786 env.close();
6787
6788 // Fund accounts
6789 env(pay(issuer, broker, mpt(10'000'000)));
6790 env(pay(issuer, borrower, mpt(1'000)));
6791 env.close();
6792
6793 // Create vault and broker
6794 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
6795 // Create a loan first (this creates debt)
6796 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
6797 env(set(borrower, brokerInfo.brokerID, 10'000),
6798 Sig(sfCounterpartySignature, broker),
6799 kLoanServiceFee(mpt(100).value()),
6800 kPaymentInterval(100),
6801 Fee(XRP(100)));
6802 env.close();
6803 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
6804 // We need coverAvailable >= (debtTotal * coverRateMinimum)
6805 // Deposit enough cover to ensure the fee goes to broker owner
6806 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
6807 // at least 1,000 cover. Default cover is 1,000, so we add more to be
6808 // safe.
6809 auto const additionalCover = mpt(50'000).value();
6810 env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover}));
6811 env.close();
6812 // Verify broker owner is authorized
6813 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
6814 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
6815 // Broker owner unauthorizes.
6816 // First, pay any positive balance to issuer to zero it out
6817 auto const brokerBalance = env.balance(broker, mpt);
6818 env(pay(broker, issuer, brokerBalance));
6819 env.close();
6820 // Then, unauthorize the MPT.
6821 mptt.authorize({.account = broker, .flags = tfMPTUnauthorize});
6822 env.close();
6823 // Verify the MPT is unauthorized.
6824 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
6825 // Now borrower tries to make a payment
6826 // We should get a tesSUCCESS instead of a tecNO_AUTH.
6827 auto const borrowerBalance = env.balance(borrower, mpt);
6828 env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
6829 env.close();
6830 // Verify the MPT is still unauthorized.
6831 BEAST_EXPECT(env.le(brokerMpt) == nullptr);
6832 // Verify the service fee went to the broker pseudo-account
6833 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
6834 BEAST_EXPECT(brokerSle))
6835 {
6836 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
6837 auto const balance = env.balance(pseudo, mpt);
6838 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
6839 BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance)));
6840 }
6841 }
6842
6843 void
6845 {
6846 testcase << "LoanPay Broker Owner without permissioned domain of the MPT";
6847 using namespace jtx;
6848 using namespace loan;
6849
6850 Account const issuer("issuer");
6851 Account const borrower("borrower");
6852 Account const broker("broker");
6853
6854 Env env{*this, features};
6855 env.fund(XRP(20'000), issuer, broker, borrower);
6856 env.close();
6857
6858 auto credType = "credential1";
6859
6860 pdomain::Credentials const credentials1 = {{.issuer = issuer, .credType = credType}};
6861 env(pdomain::setTx(issuer, credentials1));
6862 env.close();
6863
6864 auto domainID = pdomain::getNewDomain(env.meta());
6865
6866 env(credentials::create(broker, issuer, credType));
6867 env(credentials::accept(broker, issuer, credType));
6868 env.close();
6869
6870 env(credentials::create(borrower, issuer, credType));
6871 env(credentials::accept(borrower, issuer, credType));
6872 env.close();
6873
6874 MPTTester mptt{env, issuer, kMptInitNoFund};
6875 mptt.create({
6876 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
6877 .domainID = domainID,
6878 });
6879
6880 PrettyAsset const mpt{mptt.issuanceID()};
6881
6882 // Authorize broker and borrower
6883 mptt.authorize({.account = broker});
6884 mptt.authorize({.account = borrower});
6885
6886 env.close();
6887
6888 // Fund accounts
6889 env(pay(issuer, broker, mpt(10'000'000)));
6890 env(pay(issuer, borrower, mpt(1'000)));
6891 env.close();
6892
6893 // Create vault and broker
6894 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
6895 // Create a loan first (this creates debt)
6896 auto const keylet = keylet::loan(brokerInfo.brokerID, 1);
6897 env(set(borrower, brokerInfo.brokerID, 10'000),
6898 Sig(sfCounterpartySignature, broker),
6899 kLoanServiceFee(mpt(100).value()),
6900 kPaymentInterval(100),
6901 Fee(XRP(100)));
6902 env.close();
6903 // Ensure broker has sufficient cover so brokerPayee == brokerOwner
6904 // We need coverAvailable >= (debtTotal * coverRateMinimum)
6905 // Deposit enough cover to ensure the fee goes to broker owner
6906 // The default coverRateMinimum is 10%, so for a 10,000 loan we need
6907 // at least 1,000 cover. Default cover is 1,000, so we add more to be
6908 // safe.
6909 auto const additionalCover = mpt(50'000).value();
6910 env(loanBroker::coverDeposit(broker, brokerInfo.brokerID, STAmount{mpt, additionalCover}));
6911 env.close();
6912 // Verify broker owner is authorized
6913 auto const brokerMpt = keylet::mptoken(mptt.issuanceID(), broker);
6914 BEAST_EXPECT(env.le(brokerMpt) != nullptr);
6915 // Remove the credentials for the Broker owner.
6916 // First, pay any positive balance to issuer to zero it out
6917 auto const brokerBalance = env.balance(broker, mpt);
6918 env(pay(broker, issuer, brokerBalance));
6919 env.close();
6920
6921 env(credentials::deleteCred(broker, broker, issuer, credType));
6922 env.close();
6923
6924 // Make sure the broker is not authorized to hold the MPT after we
6925 // deleted the credentials
6926 env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
6927
6928 // Now borrower tries to make a payment
6929 // We should get a tesSUCCESS instead of a tecNO_AUTH.
6930 auto const borrowerBalance = env.balance(borrower, mpt);
6931 env(pay(borrower, keylet.key, mpt(10'100)), Fee(XRP(100)), Ter(tesSUCCESS));
6932 env.close();
6933 // Verify broker is still not authorized
6934 env(pay(issuer, broker, mpt(1'000)), Ter(tecNO_AUTH));
6935 // Verify the service fee went to the broker pseudo-account
6936 if (auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
6937 BEAST_EXPECT(brokerSle))
6938 {
6939 Account const pseudo("pseudo-account", brokerSle->at(sfAccount));
6940 auto const balance = env.balance(pseudo, mpt);
6941 // 1,000 default + 50,000 extra + 100 service fee from LoanPay
6942 BEAST_EXPECTS(balance == mpt(51'100), to_string(json::Value(balance)));
6943 }
6944 }
6945
6946 void
6948 {
6949 testcase << "LoanSet Broker Owner without permissioned domain of the MPT";
6950 using namespace jtx;
6951 using namespace loan;
6952
6953 Account const issuer("issuer");
6954 Account const borrower("borrower");
6955 Account const broker("broker");
6956
6957 Env env{*this, features};
6958 env.fund(XRP(20'000), issuer, broker, borrower);
6959 env.close();
6960
6961 auto credType = "credential1";
6962
6963 pdomain::Credentials const credentials1{{.issuer = issuer, .credType = credType}};
6964 env(pdomain::setTx(issuer, credentials1));
6965 env.close();
6966
6967 auto domainID = pdomain::getNewDomain(env.meta());
6968
6969 // Add credentials for the broker and borrower
6970 env(credentials::create(broker, issuer, credType));
6971 env(credentials::accept(broker, issuer, credType));
6972 env.close();
6973
6974 env(credentials::create(borrower, issuer, credType));
6975 env(credentials::accept(borrower, issuer, credType));
6976 env.close();
6977
6978 MPTTester mptt{env, issuer, kMptInitNoFund};
6979 mptt.create({
6980 .flags = tfMPTCanClawback | tfMPTRequireAuth | tfMPTCanTransfer | tfMPTCanLock,
6981 .domainID = domainID,
6982 });
6983
6984 PrettyAsset const mpt{mptt.issuanceID()};
6985
6986 // Authorize broker and borrower
6987 mptt.authorize({.account = broker});
6988 mptt.authorize({.account = borrower});
6989 env.close();
6990
6991 // Fund accounts
6992 env(pay(issuer, broker, mpt(10'000'000)));
6993 env(pay(issuer, borrower, mpt(1'000)));
6994 env.close();
6995
6996 // Create vault and broker
6997 auto const brokerInfo = createVaultAndBroker(env, mpt, broker);
6998
6999 // Remove the credentials for the Broker owner.
7000 // Clear the balance first.
7001 auto const brokerBalance = env.balance(broker, mpt);
7002 env(pay(broker, issuer, brokerBalance));
7003 env.close();
7004 // Delete the credentials
7005 env(credentials::deleteCred(broker, broker, issuer, credType));
7006 env.close();
7007
7008 // Create a loan, this should fail for tecNO_AUTH
7009 env(set(borrower, brokerInfo.brokerID, 10'000),
7010 Sig(sfCounterpartySignature, broker),
7011 kLoanServiceFee(mpt(100).value()),
7012 kPaymentInterval(100),
7013 Fee(XRP(100)),
7014 Ter(tecNO_AUTH));
7015 env.close();
7016 }
7017
7018 void
7020 {
7021 testcase << "First-Loss Capital Depletion on Sequential Defaults";
7022
7023 using namespace jtx;
7024 using namespace loan;
7025 using namespace loanBroker;
7026
7027 Env env{*this, features};
7028
7029 Account const issuer{"issuer"};
7030 Account const lender{"lender"};
7031 Account const borrowerA{"borrowerA"};
7032 Account const borrowerB{"borrowerB"};
7033
7034 env.fund(XRP(1'000'000), issuer, lender, borrowerA, borrowerB);
7035 env.close();
7036
7037 PrettyAsset const asset = xrpIssue();
7038 auto const vaultDepositAmount =
7039 asset(200'000); // Enough for 2 x 50k loans plus interest/fees
7040
7041 auto const brokerInfo = createVaultAndBroker(
7042 env,
7043 asset,
7044 lender,
7045 {
7046 .vaultDeposit = vaultDepositAmount.value(),
7047 .debtMax = 0,
7048 .coverRateMin = TenthBips32(20000), // 20%
7049 .coverDeposit = 21'000,
7050 .managementFeeRate = TenthBips16(100), // 0.1%
7051 .coverRateLiquidation = TenthBips32(100000),
7052 });
7053 auto const brokerKeylet = brokerInfo.brokerKeylet();
7054
7055 // Create two identical loans: each 50,000 XRP principal (scaled down to
7056 // avoid funding issues) Total DebtTotal will be ~100,000 XRP (principal
7057 // + interest) Formula will calculate cover as: 100% × (20% × 100,000) =
7058 // 20,000 XRP So we need FLC = 20,000 XRP to be fully consumed by first
7059 // default
7060 auto const principalAmount = Number(50'000);
7061 auto const loanPaymentInterval = 2592000; // 30 days
7062 auto const loanGracePeriod = 604800; // 7 days
7063
7064 // Create Loan A
7065 auto loanATx = env.jt(
7066 set(borrowerA, brokerKeylet.key, principalAmount),
7067 Sig(sfCounterpartySignature, lender),
7068 kInterestRate(TenthBips32(500)), // 5%
7069 kPaymentTotal(12),
7070 loan::kPaymentInterval(loanPaymentInterval),
7071 loan::kGracePeriod(loanGracePeriod),
7072 Fee(XRP(10))); // Sufficient fee for multi-sig transaction
7073 env(loanATx);
7074 env.close();
7075
7076 auto const loanAKeylet = keylet::loan(brokerKeylet.key, 1);
7077
7078 // Create Loan B
7079 auto loanBTx = env.jt(
7080 set(borrowerB, brokerKeylet.key, principalAmount),
7081 Sig(sfCounterpartySignature, lender),
7082 kInterestRate(TenthBips32(500)), // 5%
7083 kPaymentTotal(12),
7084 loan::kPaymentInterval(loanPaymentInterval),
7085 loan::kGracePeriod(loanGracePeriod),
7086 Fee(XRP(10))); // Sufficient fee for multi-sig transaction
7087 env(loanBTx);
7088 env.close();
7089
7090 auto const loanBKeylet = keylet::loan(brokerKeylet.key, 2);
7091
7092 auto loanASle = env.le(loanAKeylet);
7093 if (!BEAST_EXPECT(loanASle))
7094 return;
7095
7096 // Advance time past grace period for both loans to be defaultable
7097 auto const loanANextDue = loanASle->at(sfNextPaymentDueDate);
7098 auto const loanAGrace = loanASle->at(sfGracePeriod);
7099 env.close(std::chrono::seconds{loanANextDue + loanAGrace + 60});
7100
7101 env(manage(lender, loanAKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
7102 env.close();
7103
7104 // Verify Loan A is defaulted
7105 loanASle = env.le(loanAKeylet);
7106 if (!BEAST_EXPECT(loanASle))
7107 return;
7108 BEAST_EXPECT(loanASle->isFlag(lsfLoanDefault));
7109 BEAST_EXPECT(loanASle->at(sfPaymentRemaining) == 0);
7110
7111 // Check broker state after first default (from committed ledger)
7112 auto brokerSle = env.le(brokerKeylet);
7113 if (!BEAST_EXPECT(brokerSle))
7114 return;
7115 auto const afterFirstDebtTotal = brokerSle->at(sfDebtTotal);
7116 auto const afterFirstCoverAvailable = brokerSle->at(sfCoverAvailable);
7117
7118 // DebtTotal should have decreased by Loan A's debt
7119 BEAST_EXPECT(afterFirstDebtTotal == 50'134);
7120
7121 // CoverAvailable should have decreased significantly
7122 BEAST_EXPECT(afterFirstCoverAvailable == 946);
7123
7124 env(manage(lender, loanBKeylet.key, tfLoanDefault), Ter(tesSUCCESS));
7125
7126 brokerSle = env.le(brokerKeylet);
7127 if (!BEAST_EXPECT(brokerSle))
7128 return;
7129 auto const afterSecondDebtTotal = brokerSle->at(sfDebtTotal);
7130 auto const afterSecondCoverAvailable = brokerSle->at(sfCoverAvailable);
7131
7132 BEAST_EXPECT(afterSecondDebtTotal == 0);
7133
7134 BEAST_EXPECT(afterSecondCoverAvailable == 0);
7135 }
7136
7137 void
7139 {
7140 testcase("Rounding manipulation does not permit yield theft");
7141 using namespace jtx;
7142 using namespace loan;
7143
7144 // 1. Setup Environment
7145 Env env(*this, all_);
7146 Account const issuer{"issuer"};
7147 Account const lender{"lender"};
7148 Account const borrower{"borrower"};
7149
7150 env.fund(XRP(1000), issuer, lender, borrower);
7151 env.close();
7152
7153 // 2. Asset Selection
7154 PrettyAsset const iou = issuer["USD"];
7155 env(trust(lender, iou(100'000'000)));
7156 env(trust(borrower, iou(100'000'000)));
7157 env(pay(issuer, lender, iou(100'000'000)));
7158 env(pay(issuer, borrower, iou(100'000'000)));
7159 env.close();
7160
7161 // 3. Create Vault and Broker with High Debt Limit (100M)
7162 auto const brokerInfo = createVaultAndBroker(
7163 env,
7164 iou,
7165 lender,
7166 {
7167 .vaultDeposit = 5'000'000,
7168 .debtMax = Number{100'000'000},
7169 .coverDeposit = 500'000,
7170 });
7171 auto const [currentSeq, vaultKeylet] = [&]() {
7172 auto const brokerSle = env.le(keylet::loanBroker(brokerInfo.brokerID));
7173 if (!BEAST_EXPECT(brokerSle))
7174 return std::make_tuple(0u, keylet::unchecked(beast::kZero));
7175 auto const currentSeq = brokerSle->at(sfLoanSequence);
7176 auto const vaultKeylet = keylet::vault(brokerSle->at(sfVaultID));
7177 return std::make_tuple(currentSeq, vaultKeylet);
7178 }();
7179
7180 // 4. Loan Parameters (Attack Vector)
7181 Number const principal = 1'000'000;
7182 TenthBips32 const interestRate = TenthBips32{1}; // 0.001%
7183 std::uint32_t const paymentInterval = 86400;
7184 std::uint32_t const paymentTotal = 3650;
7185
7186 auto const loanSetFee = Fee(env.current()->fees().base * 2);
7187 env(set(borrower, brokerInfo.brokerID, iou(principal).value(), flags),
7188 Sig(sfCounterpartySignature, lender),
7189 loan::kInterestRate(interestRate),
7190 loan::kPaymentInterval(paymentInterval),
7191 loan::kPaymentTotal(paymentTotal),
7192 Fee(loanSetFee));
7193 env.close();
7194
7195 // --- RETRIEVE OBJECTS & SETUP ATTACK ---
7196
7197 auto borrowerBalance = [&]() { return env.balance(borrower, iou); };
7198 auto const borrowerScale = static_cast<STAmount const&>(borrowerBalance()).exponent();
7199
7200 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, currentSeq);
7201 auto const maybePeriodicPayment = [&]() -> std::optional<STAmount> {
7202 auto const loanSle = env.le(loanKeylet);
7203 if (!BEAST_EXPECT(loanSle))
7204 return std::nullopt;
7205 // Construct Payment
7206 return STAmount{iou, loanSle->at(sfPeriodicPayment)};
7207 }();
7208 if (!maybePeriodicPayment)
7209 return;
7210 auto const periodicPayment = *maybePeriodicPayment;
7211 auto const roundedPayment =
7212 roundToScale(periodicPayment, borrowerScale, Number::RoundingMode::Upward);
7213
7214 // ATTACK: Add dust buffer (1e-9) to force 'excess' logic execution
7215 STAmount const paymentBuffer{iou, Number(1, -9)};
7216 STAmount const attackPayment = periodicPayment + paymentBuffer;
7217
7218 auto const maybeInitialVaultAssets = [&]() -> std::optional<Number> {
7219 auto const vault = env.le(vaultKeylet);
7220 if (!BEAST_EXPECT(vault))
7221 return std::nullopt;
7222 return vault->at(sfAssetsTotal);
7223 }();
7224 if (!maybeInitialVaultAssets)
7225 return;
7226 auto const initialVaultAssets = *maybeInitialVaultAssets;
7227
7228 // 5. Execution Loop
7229 int yieldTheftCount = 0;
7230 auto previousAssetsTotal = initialVaultAssets;
7231
7232 for (int i = 0; i < 100; ++i)
7233 {
7234 auto const balanceBefore = borrowerBalance();
7235 env(pay(borrower, loanKeylet.key, attackPayment, flags));
7236 env.close();
7237 auto const borrowerDelta = balanceBefore - borrowerBalance();
7238 BEAST_EXPECT(borrowerDelta.signum() == roundedPayment.signum());
7239
7240 auto const loanSle = env.le(loanKeylet);
7241 if (!BEAST_EXPECT(loanSle))
7242 break;
7243 auto const updatedPayment = STAmount{iou, loanSle->at(sfPeriodicPayment)};
7244 BEAST_EXPECT(
7245 (roundToScale(updatedPayment, borrowerScale, Number::RoundingMode::Upward) ==
7246 roundedPayment));
7247 BEAST_EXPECT(
7248 (updatedPayment == periodicPayment) ||
7249 (flags == tfLoanOverpayment && i >= 2 && updatedPayment < periodicPayment));
7250
7251 auto const currentVaultSle = env.le(vaultKeylet);
7252 if (!BEAST_EXPECT(currentVaultSle))
7253 break;
7254
7255 auto const currentAssetsTotal = currentVaultSle->at(sfAssetsTotal);
7256 auto const delta = currentAssetsTotal - previousAssetsTotal;
7257
7258 BEAST_EXPECT(
7259 (delta == beast::kZero && borrowerDelta <= roundedPayment) ||
7260 (delta > beast::kZero && borrowerDelta > roundedPayment));
7261
7262 // If tx succeeded but Assets Total didn't change, interest was
7263 // stolen.
7264 if (delta == beast::kZero && borrowerDelta > roundedPayment)
7265 {
7266 yieldTheftCount++;
7267 }
7268
7269 previousAssetsTotal = currentAssetsTotal;
7270 }
7271
7272 BEAST_EXPECTS(yieldTheftCount == 0, std::to_string(yieldTheftCount));
7273 }
7274
7275 // Tests that vault withdrawals work correctly when the vault has unrealized
7276 // loss from an impaired loan, ensuring the invariant check properly
7277 // accounts for the loss.
7278 void
7280 {
7281 using namespace jtx;
7282 using namespace loan;
7283 using namespace std::chrono_literals;
7284
7285 testcase("Vault withdraw reflects sfLossUnrealized");
7286
7287 // Test constants
7288 static constexpr std::int64_t kInitialFunding = 1'000'000;
7289 static constexpr std::int64_t kLenderInitialIou = 5'000'000;
7290 static constexpr std::int64_t kDepositorInitialIou = 1'000'000;
7291 static constexpr std::int64_t kBorrowerInitialIou = 100'000;
7292 static constexpr std::int64_t kDepositAmount = 5'000;
7293 static constexpr std::int64_t kPrincipalAmount = 99;
7294 static constexpr std::uint64_t kExpectedSharesPerDepositor = 5'000'000'000;
7295 static constexpr std::uint32_t kLocalPaymentInterval = 600;
7296 static constexpr std::uint32_t kLocalPaymentTotal = 2;
7297
7298 Env env{*this, features};
7299
7300 // Setup accounts
7301 Account const issuer{"issuer"};
7302 Account const lender{"lender"};
7303 Account const depositorA{"lpA"};
7304 Account const depositorB{"lpB"};
7305 Account const borrower{"borrowerA"};
7306
7307 env.fund(XRP(kInitialFunding), issuer, lender, depositorA, depositorB, borrower);
7308 env.close();
7309
7310 // Setup trust lines
7311 PrettyAsset const iouAsset = issuer[iouCurrency_];
7312 env(trust(lender, iouAsset(10'000'000)));
7313 env(trust(depositorA, iouAsset(10'000'000)));
7314 env(trust(depositorB, iouAsset(10'000'000)));
7315 env(trust(borrower, iouAsset(10'000'000)));
7316 env.close();
7317
7318 // Fund accounts with IOUs
7319 env(pay(issuer, lender, iouAsset(kLenderInitialIou)));
7320 env(pay(issuer, depositorA, iouAsset(kDepositorInitialIou)));
7321 env(pay(issuer, depositorB, iouAsset(kDepositorInitialIou)));
7322 env(pay(issuer, borrower, iouAsset(kBorrowerInitialIou)));
7323 env.close();
7324
7325 // Create vault and broker, then add deposits from two depositors
7326 auto const broker = createVaultAndBroker(env, iouAsset, lender);
7327 Vault v{env};
7328
7329 env(v.deposit({
7330 .depositor = depositorA,
7331 .id = broker.vaultKeylet().key,
7332 .amount = iouAsset(kDepositAmount),
7333 }),
7334 Ter(tesSUCCESS));
7335 env(v.deposit({
7336 .depositor = depositorB,
7337 .id = broker.vaultKeylet().key,
7338 .amount = iouAsset(kDepositAmount),
7339 }),
7340 Ter(tesSUCCESS));
7341 env.close();
7342
7343 // Create a loan
7344 auto const sleBroker = env.le(keylet::loanBroker(broker.brokerID));
7345 if (!BEAST_EXPECT(sleBroker))
7346 return;
7347
7348 auto const loanKeylet = keylet::loan(broker.brokerID, sleBroker->at(sfLoanSequence));
7349
7350 env(set(borrower, broker.brokerID, kPrincipalAmount),
7351 Sig(sfCounterpartySignature, lender),
7352 kPaymentTotal(kLocalPaymentTotal),
7353 kPaymentInterval(kLocalPaymentInterval),
7354 Fee(env.current()->fees().base * 2),
7355 Ter(tesSUCCESS));
7356 env.close();
7357
7358 // Impair the loan to create unrealized loss
7359 env(manage(lender, loanKeylet.key, tfLoanImpair), Ter(tesSUCCESS));
7360 env.close();
7361
7362 // Verify unrealized loss is recorded in the vault
7363 auto const vaultAfterImpair = env.le(broker.vaultKeylet());
7364 if (!BEAST_EXPECT(vaultAfterImpair))
7365 return;
7366
7367 BEAST_EXPECT(
7368 vaultAfterImpair->at(sfLossUnrealized) == broker.asset(kPrincipalAmount).value());
7369
7370 // Helper to get share balance for a depositor
7371 auto const shareAsset = vaultAfterImpair->at(sfShareMPTID);
7372 auto const getShareBalance = [&](Account const& depositor) -> std::uint64_t {
7373 auto const token = env.le(keylet::mptoken(shareAsset, depositor.id()));
7374 return token ? token->getFieldU64(sfMPTAmount) : 0;
7375 };
7376
7377 // Verify both depositors have equal shares
7378 auto const sharesLpA = getShareBalance(depositorA);
7379 auto const sharesLpB = getShareBalance(depositorB);
7380 BEAST_EXPECT(sharesLpA == kExpectedSharesPerDepositor);
7381 BEAST_EXPECT(sharesLpB == kExpectedSharesPerDepositor);
7382 BEAST_EXPECT(sharesLpA == sharesLpB);
7383
7384 // Helper to attempt withdrawal
7385 auto const attemptWithdrawShares = [&](Account const& depositor,
7386 std::uint64_t shareAmount,
7387 TER expected) {
7388 STAmount const shareAmt{MPTIssue{shareAsset}, Number(shareAmount)};
7389 env(v.withdraw(
7390 {.depositor = depositor, .id = broker.vaultKeylet().key, .amount = shareAmt}),
7391 Ter(expected));
7392 env.close();
7393 };
7394
7395 // Regression test: Both depositors should successfully withdraw despite
7396 // unrealized loss. Previously failed with invariant violation:
7397 // "withdrawal must change vault and destination balance by equal
7398 // amount". This was caused by sharesToAssetsWithdraw rounding down,
7399 // creating a mismatch where vaultDeltaAssets * -1 != destinationDelta
7400 // when unrealized loss exists.
7401 attemptWithdrawShares(depositorA, sharesLpA, tesSUCCESS);
7402 attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
7403 }
7404
7405 // A residual overpayment can reduce the stored principal by one scale-unit
7406 // *less* than computeOverpaymentComponents predicts, firing the
7407 // "principal change agrees" XRPL_ASSERT_PARTS in doOverpayment:
7408 //
7409 // trackedPrincipalDelta == principalOutstanding - newPrincipalOutstanding
7410 //
7411 // tryOverpayment re-amortizes the loan at the reduced principal, then
7412 // re-derives the theoretical principal from the new periodic payment via
7413 // (P * paymentFactor) / paymentFactor. That round-trip is not exact in
7414 // Number's 19-digit arithmetic; a positive residual pushes the recomputed
7415 // principal a hair above the exact grid point `oldPrincipal - delta`, and
7416 // the Upward rounding in tryOverpayment then bumps it a full scale-unit
7417 // higher. The principal therefore drops by `delta - 1 unit`, not `delta`.
7418 //
7419 // Concrete case (isolated, at the tryOverpayment level):
7420 // A 100 USD loan at the minimum non-zero rate, 3 payments, loanScale -10.
7421 // After one regular payment (principalOutstanding 66.6666666674) a residual overpayment of
7422 // 0.049999998 yields trackedPrincipalDelta 0.048999998 but only reduces the principal by
7423 // 0.0489999979 (newPrincipal 66.6176666695) — short by 1e-10.
7424 //
7425 // With fixCleanup3_2_0, tryOverpayment pins the new principal to the exact,
7426 // on-grid reduction (oldPrincipal - trackedPrincipalDelta) instead of the
7427 // lossy (P*factor)/factor round-trip, so the assertion holds and the
7428 // overpayment applies cleanly. The three "principal change agrees" /
7429 // "interest paid agrees" / "principal payment matches" assertions are
7430 // gated behind the same amendment, so without it they are disabled (the
7431 // server does not abort) and the loan keeps the pre-amendment computation.
7432 //
7433 // The test runs the same scenario under both amendment settings and checks
7434 // the stored principal against a ground-truth value derived independently of
7435 // the loan-state computation under test.
7436 void
7438 {
7439 testcase("bug: doOverpayment asserts 'principal change agrees'");
7440
7441 using namespace jtx;
7442 using namespace loan;
7443 using namespace xrpl::detail;
7444
7445 struct Params
7446 {
7447 TenthBips32 interestRate;
7448 TenthBips16 managementFeeRate;
7449 std::uint32_t paymentTotal;
7450 std::uint32_t paymentInterval;
7451 std::int64_t principal;
7452 Number overpayment;
7453 TenthBips32 overpaymentInterestRate;
7454 TenthBips32 overpaymentFeeRate;
7455 std::optional<int> vaultScale;
7456 };
7457
7458 struct Result
7459 {
7460 Number principalOutstanding; // stored principal after the LoanPay
7461 Number expectedNewPrincipal; // ground truth, independent of the fix
7462 Number managementFeeChange; // managementFeeOutstanding after - before
7463 Number unit; // one scale-unit at the loan scale
7464 };
7465
7466 auto runScenario = [this](FeatureBitset features, Params const& p) -> Result {
7467 Env env(*this, features);
7468
7469 Account const issuer{"issuer"};
7470 Account const lender{"vaultOwner"};
7471 Account const borrower{"borrower"};
7472
7473 env.fund(XRP(1'000'000), issuer, lender, borrower);
7474 env(fset(issuer, asfDefaultRipple));
7475 env.close();
7476
7477 PrettyAsset const iouAsset = issuer["USD"];
7478 Asset const asset = iouAsset.raw();
7479 STAmount const iouLimit{asset, Number{9'999'999'999'999'999LL}};
7480 env(trust(lender, iouLimit));
7481 env(trust(borrower, iouLimit));
7482 env(pay(issuer, lender, iouAsset(1'000'000)));
7483 env(pay(issuer, borrower, iouAsset(1'000'000)));
7484 env.close();
7485
7486 auto const broker = createVaultAndBroker(
7487 env,
7488 iouAsset,
7489 lender,
7490 {.vaultDeposit = 900'000,
7491 .debtMax = 0,
7492 .managementFeeRate = p.managementFeeRate,
7493 .vaultScale = p.vaultScale});
7494
7495 auto const brokerSle = env.le(broker.brokerKeylet());
7496 BEAST_EXPECT(brokerSle);
7497 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
7498 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
7499
7500 env(set(borrower, broker.brokerID, Number{p.principal}, tfLoanOverpayment),
7501 Sig(sfCounterpartySignature, lender),
7502 kInterestRate(p.interestRate),
7503 kPaymentTotal(p.paymentTotal),
7504 kPaymentInterval(p.paymentInterval),
7505 kGracePeriod(p.paymentInterval),
7506 kOverpaymentFee(p.overpaymentFeeRate),
7507 kOverpaymentInterestRate(p.overpaymentInterestRate),
7508 Fee(env.current()->fees().base * 2),
7509 Ter(tesSUCCESS));
7510 env.close();
7511
7512 // The single LoanPay below makes one regular payment (the overpayment
7513 // is smaller than one period) and leaves the residual as an
7514 // overpayment.
7515 auto const s = getCurrentState(env, broker, loanKeylet);
7516 auto const periodicRate = loanPeriodicRate(s.interestRate, s.paymentInterval);
7517 auto const onePeriod = computePaymentComponents(
7518 env.current()->rules(),
7519 asset,
7520 s.loanScale,
7521 s.totalValue,
7522 s.principalOutstanding,
7523 s.managementFeeOutstanding,
7524 s.periodicPayment,
7525 periodicRate,
7526 s.paymentRemaining,
7527 p.managementFeeRate);
7528
7529 // Ground truth: the stored principal must drop by exactly the regular
7530 // payment's principal portion plus the overpayment's principal
7531 // portion. computeOverpaymentComponents depends only on the
7532 // overpayment amount and rates (not on the loan-state computation
7533 // under test), so it is an independent oracle. Both components are
7534 // computed under the same rules as the env so the payment factor
7535 // matches.
7536 auto const overpaymentComponents = computeOverpaymentComponents(
7537 env.current()->rules(),
7538 asset,
7539 s.loanScale,
7540 p.overpayment,
7541 p.overpaymentInterestRate,
7542 p.overpaymentFeeRate,
7543 p.managementFeeRate);
7544 Number const expectedNewPrincipal = s.principalOutstanding -
7545 onePeriod.trackedPrincipalDelta - overpaymentComponents.trackedPrincipalDelta;
7546
7547 Number const managementFeeBefore = s.managementFeeOutstanding;
7548
7549 STAmount const payAmount{asset, onePeriod.trackedValueDelta + p.overpayment};
7550 env(pay(borrower, loanKeylet.key, payAmount),
7551 Txflags(tfLoanOverpayment),
7552 Ter(tesSUCCESS));
7553 env.close();
7554
7555 auto const loanSle = env.le(loanKeylet);
7556 BEAST_EXPECT(loanSle);
7557
7558 return Result{
7559 .principalOutstanding = loanSle ? Number{loanSle->at(sfPrincipalOutstanding)} : 0,
7560 .expectedNewPrincipal = expectedNewPrincipal,
7561 .managementFeeChange =
7562 (loanSle ? Number{loanSle->at(sfManagementFeeOutstanding)} : Number{0}) -
7563 managementFeeBefore,
7564 .unit = Number{1, s.loanScale}};
7565 };
7566
7567 // Scenario 1: the original near-zero-rate principal reproduction
7568 // (loanScale -10, no management fee). 0.049999998 is smaller than one
7569 // period, so it stays a residual overpayment.
7570 Params const principalCase{
7571 .interestRate = TenthBips32{1},
7572 .managementFeeRate = TenthBips16{0},
7573 .paymentTotal = 3,
7574 .paymentInterval = 60,
7575 .principal = 100,
7576 .overpayment = Number{49999998, -9},
7577 .overpaymentInterestRate = TenthBips32{1000},
7578 .overpaymentFeeRate = TenthBips32{1000},
7579 .vaultScale = 1};
7580
7581 // With fixCleanup3_2_0 the stored principal lands exactly on the
7582 // ground-truth grid point: it is reduced by exactly the overpayment's
7583 // principal portion. This is the key correctness check: if the principal
7584 // pin were removed (even with the assertions still gated off), the lossy
7585 // (P * factor) / factor round-trip would leave the principal one
7586 // scale-unit high and this would fail.
7587 Result const fixed = runScenario(all_, principalCase);
7588 BEAST_EXPECTS(
7589 fixed.principalOutstanding == fixed.expectedNewPrincipal,
7590 "fixed principal " + to_string(fixed.principalOutstanding) + " != expected " +
7591 to_string(fixed.expectedNewPrincipal));
7592
7593 // Without the amendment the loan amortizes with the catastrophically
7594 // cancelling near-zero payment factor, so its schedule (and ground truth)
7595 // differ from the fixed case; the gated assertions keep the server from
7596 // aborting and the overpayment still lands exactly on that schedule.
7597 Result const legacy = runScenario(all_ - fixCleanup3_2_0, principalCase);
7598 BEAST_EXPECTS(
7599 legacy.principalOutstanding == legacy.expectedNewPrincipal,
7600 "legacy principal " + to_string(legacy.principalOutstanding) + " != expected " +
7601 to_string(legacy.expectedNewPrincipal));
7602
7603 // Scenario 2: a normal-rate loan with a 10% management fee. At a normal
7604 // rate the payment factor is identical across the amendment, so toggling
7605 // fixCleanup3_2_0 isolates the fix. This overpayment (found by search)
7606 // lands on a state where both the principal and the management fee differ
7607 // by one scale-unit between the fixed and legacy paths.
7608 Params const feeCase{
7609 .interestRate = TenthBips32{10000},
7610 .managementFeeRate = TenthBips16{10000},
7611 .paymentTotal = 6,
7612 .paymentInterval = 30u * 24 * 60 * 60,
7613 .principal = 1000,
7614 .overpayment = Number{214367363, -10},
7615 .overpaymentInterestRate = TenthBips32{0},
7616 .overpaymentFeeRate = TenthBips32{0},
7617 .vaultScale = std::nullopt};
7618
7619 Result const feeFixed = runScenario(all_, feeCase);
7620 Result const feeLegacy = runScenario(all_ - fixCleanup3_2_0, feeCase);
7621
7622 // With the fix the principal is the exact reduction; without it the lossy
7623 // (P * factor) / factor round-trip leaves it one scale-unit high.
7624 BEAST_EXPECTS(
7625 feeFixed.principalOutstanding == feeFixed.expectedNewPrincipal,
7626 "fee-case fixed principal " + to_string(feeFixed.principalOutstanding) +
7627 " != expected " + to_string(feeFixed.expectedNewPrincipal));
7628 BEAST_EXPECTS(
7629 feeLegacy.principalOutstanding == feeLegacy.expectedNewPrincipal + feeLegacy.unit,
7630 "fee-case legacy principal " + to_string(feeLegacy.principalOutstanding) +
7631 " != expected " + to_string(feeLegacy.expectedNewPrincipal + feeLegacy.unit));
7632
7633 // Management fee: the overpayment re-amortizes a fee-bearing loan, so the management fee
7634 // outstanding drops.
7635 //
7636 // Unlike the principal that is already at the correct precision, the re-amortized
7637 // management fee is tenthBipsOfValue of the new schedule's gross interest, which depends
7638 // on the recomputed periodic payment. So the expected change below is a pinned constant
7639 // captured from a passing run a magic value only because there is nothing simpler to
7640 // compare against.
7641 //
7642 // At the integration level, toggling the amendment also changes the regular payment's
7643 // rounding so a fixed-vs-legacy comparison cannot isolate the overpayment management-fee
7644 // fix.
7645 BEAST_EXPECT(feeFixed.managementFeeChange == feeLegacy.managementFeeChange);
7646 BEAST_EXPECTS(
7647 (feeFixed.managementFeeChange == Number{-8219709543, -10}),
7648 "fee-case mgmt fee change " + to_string(feeFixed.managementFeeChange));
7649 }
7650
7651 // A LoanSet with InterestRate = 1 (0.001% annualized, the minimum non-zero
7652 // rate). At such a near-zero rate the closed-form payment factor
7653 // (1 + r)^n - 1 cancels catastrophically.
7654 //
7655 // Without fixCleanup3_2_0 the resulting amortization is degenerate and the
7656 // LoanSet is rejected with tecPRECISION_LOSS (no loan created). With the
7657 // amendment, computePowerMinusOneHybrid uses a numerically-stable series
7658 // expansion, so the loan is created and the scheduled payments
7659 // (2 * periodicPayment) cover the principal — no economic underpayment
7660 // (yield theft).
7661 //
7662 // The test runs the same LoanSet under both amendment settings and pins the
7663 // exact outcome for each.
7664 void
7666 {
7667 testcase("LoanSet near-zero interest rate covers principal");
7668
7669 using namespace jtx;
7670 using namespace loan;
7671
7672 Number const principalRequested{1000};
7673
7674 struct Result
7675 {
7676 TER ter = tesSUCCESS;
7677 bool created = false;
7678 std::int32_t loanScale = 0;
7679 Number principal;
7680 Number totalValue;
7681 Number managementFee;
7682 Number periodicPayment;
7683 };
7684
7685 auto runScenario = [&](FeatureBitset features, TER expectedTer) -> Result {
7686 Env env(*this, features);
7687
7688 Account const issuer{"issuer"};
7689 Account const lender{"vaultOwner"};
7690 Account const borrower{"borrower"};
7691
7692 env.fund(XRP(1'000'000), issuer, lender, borrower);
7693 env(fset(issuer, asfDefaultRipple));
7694 env.close();
7695
7696 PrettyAsset const iouAsset = issuer["USD"];
7697 STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}};
7698 env(trust(lender, iouLimit));
7699 env(trust(borrower, iouLimit));
7700 env(pay(issuer, lender, iouAsset(1'000'000)));
7701 env(pay(issuer, borrower, iouAsset(1'000'000)));
7702 env.close();
7703
7704 auto const broker = createVaultAndBroker(
7705 env,
7706 iouAsset,
7707 lender,
7708 {.vaultDeposit = 100'000, .debtMax = 0, .managementFeeRate = TenthBips16{0}});
7709
7710 auto const brokerSle = env.le(broker.brokerKeylet());
7711 BEAST_EXPECT(brokerSle);
7712 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
7713 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
7714
7715 env(set(borrower, broker.brokerID, principalRequested),
7716 Sig(sfCounterpartySignature, lender),
7717 kInterestRate(TenthBips32{1}),
7718 kPaymentTotal(2),
7719 kPaymentInterval(400),
7720 Fee(env.current()->fees().base * 2),
7721 Ter(expectedTer));
7722 env.close();
7723
7724 Result r;
7725 r.ter = env.ter();
7726 if (auto const loanSle = env.le(loanKeylet))
7727 {
7728 r.created = true;
7729 r.loanScale = loanSle->at(sfLoanScale);
7730 r.principal = loanSle->at(sfPrincipalOutstanding);
7731 r.totalValue = loanSle->at(sfTotalValueOutstanding);
7732 r.managementFee = loanSle->at(sfManagementFeeOutstanding);
7733 r.periodicPayment = loanSle->at(sfPeriodicPayment);
7734 }
7735 return r;
7736 };
7737
7738 Result const fixed = runScenario(all_, tesSUCCESS);
7739 Result const legacy = runScenario(all_ - fixCleanup3_2_0, tecPRECISION_LOSS);
7740
7741 // Without the amendment, the catastrophically-cancelling closed-form
7742 // payment factor produces a degenerate amortization that fails
7743 // checkLoanGuards: the LoanSet is rejected with tecPRECISION_LOSS and no
7744 // loan is created.
7745 BEAST_EXPECT(legacy.ter == tecPRECISION_LOSS);
7746 BEAST_EXPECT(!legacy.created);
7747
7748 // With the amendment the stable series expansion produces a valid loan
7749 // at loanScale -10.
7750 BEAST_EXPECT(fixed.ter == tesSUCCESS);
7751 BEAST_EXPECT(fixed.created);
7752 BEAST_EXPECT(fixed.loanScale == -10);
7753 BEAST_EXPECT(fixed.principal == principalRequested);
7754 BEAST_EXPECT((fixed.totalValue == Number{10000000001903, -10}));
7755 BEAST_EXPECT(fixed.managementFee == beast::kZero);
7756
7757 // Periodic payment from the numerically-stable series expansion, and the
7758 // scheduled total (2 * periodicPayment) which exceeds the 1000 principal
7759 // — no economic underpayment / yield theft.
7760 BEAST_EXPECT((fixed.periodicPayment == Number{5000000000951293762, -16}));
7761 BEAST_EXPECT((fixed.periodicPayment * 2 == Number{1000000000190258752, -15}));
7762 BEAST_EXPECT(fixed.periodicPayment * 2 > principalRequested);
7763 }
7764
7765 // An overpayment whose residual amount has more precision than loanScale
7766 // fires the isRounded(asset, overpayment, loanScale) assertion in
7767 // computeOverpaymentComponents (and a downstream "interest paid agrees"
7768 // assertion in doOverpayment). fixCleanup3_2_0 rounds the residual down
7769 // to loanScale before passing it in. The pre-amendment path can't be
7770 // tested here because the assertion fires in Debug builds and aborts
7771 // the test process — see the PR description for context.
7772 void
7774 {
7775 testcase("bug: computeOverpaymentComponents isRounded assertion");
7776
7777 using namespace jtx;
7778 using namespace loan;
7779 Env env(*this, all_);
7780
7781 Account const issuer{"issuer"};
7782 Account const lender{"vaultOwner"};
7783 Account const borrower{"borrower"};
7784
7785 env.fund(XRP(1'000'000), issuer, lender, borrower);
7786 env(fset(issuer, asfDefaultRipple));
7787 env.close();
7788
7789 PrettyAsset const iouAsset = issuer["USD"];
7790 STAmount const iouLimit{iouAsset.raw(), Number{9'999'999'999'999'999LL}};
7791 env(trust(lender, iouLimit));
7792 env(trust(borrower, iouLimit));
7793 env(pay(issuer, lender, iouAsset(1'000'000)));
7794 env(pay(issuer, borrower, iouAsset(1'000'000)));
7795 env.close();
7796
7797 auto const broker = createVaultAndBroker(
7798 env,
7799 iouAsset,
7800 lender,
7801 {.vaultDeposit = 100'000,
7802 .debtMax = 5000,
7803 .managementFeeRate = TenthBips16{1000},
7804 .vaultScale = 1});
7805
7806 auto const sleBroker = env.le(broker.brokerKeylet());
7807 if (!BEAST_EXPECT(sleBroker))
7808 return;
7809 auto const loanSequence = sleBroker->at(sfLoanSequence);
7810 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
7811
7812 using namespace loan;
7813 env(set(borrower, broker.brokerID, Number{1000}, tfLoanOverpayment),
7814 Sig(sfCounterpartySignature, lender),
7815 kInterestRate(TenthBips32{10000}),
7816 kPaymentTotal(12),
7817 kPaymentInterval(60),
7818 kGracePeriod(60),
7819 kOverpaymentFee(TenthBips32{1000}),
7820 kOverpaymentInterestRate(TenthBips32{1000}),
7821 Fee(env.current()->fees().base * 2),
7822 Ter(tesSUCCESS));
7823 env.close();
7824
7825 // periodic * 1.5 at 15-sig-digit precision: 125.000154585042. This
7826 // has too many digits to round cleanly to loanScale=-10, so the
7827 // overpayment residual fails the isRounded check.
7828 STAmount const payAmount{iouAsset.raw(), Number{125'000'154'585'042LL, -12}};
7829 env(pay(borrower, loanKeylet.key, payAmount), Txflags(tfLoanOverpayment), Ter(tesSUCCESS));
7830 env.close();
7831 }
7832
7833 // Regression for the dual-rounding fix at coarse (integer-MPT) scale.
7834 //
7835 // Loan: P=1, r=50% (50000 tenth-bips), n=3, yearly interval. The
7836 // amortization schedule produces a fractional principal
7837 // (~0.47) which under round-to-nearest collapses to 0 in a single
7838 // step, causing `doPayment`'s strict `>` assertion on principal to
7839 // fire mid-loan. With fixCleanup3_2_0 enabled, principal is rounded
7840 // upward (sticks at 1 across the first two periods) and only clears
7841 // in the final payment.
7842 //
7843 // The test pays one period at a time across three LoanPay
7844 // transactions and verifies the loan completes (paymentRemaining=0)
7845 // with totals matching the loan's economics (1 principal + 2 interest).
7846 void
7848 {
7849 // Without fixCleanup3_2_0, this behavior will abort the server, so
7850 // don't run without it.
7851 if (!features[fixCleanup3_2_0])
7852 return;
7853
7854 testcase("edge: integer MPT principal stuck mid-loan completes via final");
7855
7856 using namespace jtx;
7857 Env env(*this, features);
7858
7859 Account const issuer{"issuer"};
7860 Account const lender{"lender"};
7861 Account const borrower{"borrower"};
7862
7863 env.fund(XRP(100'000), issuer, lender, borrower);
7864 env.close();
7865
7866 MPTTester mptt{env, issuer, kMptInitNoFund};
7867 mptt.create({.maxAmt = 100'000, .flags = tfMPTCanTransfer});
7868 PrettyAsset const asset{mptt.issuanceID()};
7869
7870 mptt.authorize({.account = lender});
7871 mptt.authorize({.account = borrower});
7872
7873 env(pay(issuer, lender, asset(10'000)));
7874 env(pay(issuer, borrower, asset(10'000)));
7875 env.close();
7876
7877 Vault const vault{env};
7878 auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
7879 env(vaultTx);
7880 env.close();
7881
7882 env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)}));
7883 env.close();
7884
7885 auto const brokerKeylet = keylet::loanBroker(lender.id(), env.seq(lender));
7886 env(loanBroker::set(lender, vaultKeylet.key),
7888 Fee(env.current()->fees().base * 2));
7889 env.close();
7890
7891 auto const brokerStateBefore = env.le(brokerKeylet);
7892 if (!BEAST_EXPECT(brokerStateBefore))
7893 return;
7894 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
7895 auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence);
7896
7897 env(loan::set(borrower, brokerKeylet.key, Number{1}),
7898 Sig(sfCounterpartySignature, lender),
7901 loan::kPaymentInterval(31'536'000),
7902 Fee(env.current()->fees().base * 2));
7903 env.close();
7904
7905 auto const borrowerStart = env.balance(borrower, asset).value();
7906
7907 // Three separate periodic payments of 1 each. Expected per-period
7908 // evolution at integer MPT scale (TVO = PO + interestDue +
7909 // managementFeeDue):
7910 // start: PO=1, TVO=3, paymentRemaining=3
7911 // after pay #1: PO=1, TVO=2, paymentRemaining=2 (principal sticks)
7912 // after pay #2: PO=1, TVO=1, paymentRemaining=1 (principal sticks)
7913 // after pay #3: PO=0, TVO=0, paymentRemaining=0 (final clears)
7914 std::array<Number, 3> const expectedPO{Number{1}, Number{1}, Number{0}};
7915 std::array<Number, 3> const expectedTVO{Number{2}, Number{1}, Number{0}};
7916 std::array<std::uint32_t, 3> const expectedRemaining{2, 1, 0};
7917
7918 for (int i = 0; i < 3; ++i)
7919 {
7920 env(loan::pay(borrower, loanKeylet.key, asset(1)), Ter(tesSUCCESS));
7921 env.close();
7922
7923 auto const sle = env.le(loanKeylet);
7924 if (!BEAST_EXPECT(sle))
7925 return;
7926 BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]);
7927 BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]);
7928 BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]);
7929 }
7930
7931 // Borrower paid 3 total regardless of fee split (1 principal + 2
7932 // interest+fee, matching loan economics).
7933 auto const borrowerEnd = env.balance(borrower, asset).value();
7934 BEAST_EXPECT(borrowerStart - borrowerEnd == asset(3).value());
7935 }
7936
7937 // A near-zero interest rate on a 100 USD loan
7938 // produces total interest of ~6 units at loanScale -9. Numerical error
7939 // in the amortization formula pushes the theoretical principal above
7940 // the theoretical value, producing a negative theoretical interest.
7941 // The payment delta then exceeds the actual outstanding interest,
7942 // violating XRPL_ASSERT_PARTS in computePaymentComponents.
7943 void
7945 {
7946 testcase("bug: LoanPay asserts 'interest due delta' on near-zero rate");
7947
7948 using namespace jtx;
7949 using namespace std::chrono_literals;
7950 Env env(*this, all_);
7951
7952 Account const issuer{"issuer"};
7953 Account const lender{"lender"};
7954 Account const borrower{"borrower"};
7955
7956 env.fund(XRP(1'000'000), issuer, lender, borrower);
7957 env.close();
7958 env(fset(issuer, asfDefaultRipple));
7959 env.close();
7960
7961 PrettyAsset const iouAsset = issuer["USD"];
7962 env(trust(lender, iouAsset(1'000'000'000)));
7963 env(trust(borrower, iouAsset(1'000'000'000)));
7964 env(pay(issuer, lender, iouAsset(5'000'000)));
7965 env(pay(issuer, borrower, iouAsset(5'000'000)));
7966 env.close();
7967
7968 BrokerParameters const brokerParams{
7969 .vaultDeposit = 1'000'000,
7970 .debtMax = 1'000'000,
7971 .coverRateMin = TenthBips32{0},
7972 .coverDeposit = 0,
7973 .managementFeeRate = TenthBips16{0},
7974 .coverRateLiquidation = TenthBips32{0}};
7975
7976 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
7977
7978 using namespace loan;
7979
7980 auto const loanSetFee = Fee(env.current()->fees().base * 2);
7981 Number const principalRequest{100};
7982
7983 auto createJson = env.json(
7984 set(borrower, broker.brokerID, principalRequest),
7985 Fee(loanSetFee),
7986 Json(sfCounterpartySignature, json::ValueType::Object));
7987
7988 createJson["InterestRate"] = 1; // minimum non-zero rate
7989 createJson["PaymentTotal"] = 3;
7990 createJson["PaymentInterval"] = 600;
7991
7992 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
7993 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
7994 auto const keylet = keylet::loan(broker.brokerID, loanSequence);
7995
7996 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
7997 env(createJson, Ter(tesSUCCESS));
7998 env.close();
7999
8000 // For principal=100, n=3 the amortization schedule produces a
8001 // periodic payment ≈ 33.33 USD. We pay 35 USD, which is more than
8002 // one period's worth — enough for the LoanPay path to enter
8003 // computePaymentComponents and reach the assertion that fires
8004 // when the bug is present. With the fix, the tx applies cleanly.
8005 env(pay(borrower, keylet.key, iouAsset(35)), Ter(tesSUCCESS));
8006 env.close();
8007 }
8008
8009 // Integration test: full lifecycle of a $1B loan in the bug regime.
8010 // Verifies that the vault collects the economically-correct interest
8011 // income and that conservation holds at the trust-line level.
8012 //
8013 // Pre-fix (closed-form `power(1+r, n) - 1`): vault collected only
8014 // ~$0.058 per $1B due to cancellation of `(1+r)^n - 1` at r*n ~ 5.7e-10.
8015 // Post-fix (hybrid binomial path): vault collects ~$0.38 per $1B,
8016 // matching the value computed independently with arbitrary-precision
8017 // Decimal arithmetic.
8018 void
8020 {
8021 testcase("integration: full loan lifecycle, vault interest at near-zero rate");
8022
8023 using namespace jtx;
8024 using namespace jtx::loan;
8025 using namespace std::chrono_literals;
8026 Env env(*this, all_);
8027
8028 Account const issuer{"issuer"};
8029 Account const lender{"lender"};
8030 Account const borrower{"borrower"};
8031
8032 env.fund(XRP(1'000'000), issuer, lender, borrower);
8033 env.close();
8034 env(fset(issuer, asfDefaultRipple));
8035 env.close();
8036
8037 PrettyAsset const iouAsset = issuer["USD"];
8038 STAmount const trustLimit{iouAsset.raw(), Number{1, 17}};
8039 env(trust(lender, trustLimit));
8040 env(trust(borrower, trustLimit));
8041 env.close();
8042 env(pay(issuer, lender, iouAsset(5'000'000'000LL)));
8043 env(pay(issuer, borrower, iouAsset(5'000'000'000LL)));
8044 env.close();
8045
8046 auto usdBalance = [&](Account const& a) {
8047 return env.balance(a, iouAsset.raw().get<Issue>()).value();
8048 };
8049 STAmount const borrowerStartBal = usdBalance(borrower);
8050
8051 BrokerParameters const brokerParams{
8052 .vaultDeposit = Number{2, 9},
8053 .debtMax = Number{0},
8054 .coverRateMin = TenthBips32{0},
8055 .coverDeposit = 0,
8056 .managementFeeRate = TenthBips16{0},
8057 .coverRateLiquidation = TenthBips32{0}};
8058 BrokerInfo const broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
8059
8060 auto const vaultBefore = env.le(broker.vaultKeylet());
8061 BEAST_EXPECT(vaultBefore);
8062 Number const vaultAvailableBefore = vaultBefore->at(sfAssetsAvailable);
8063
8064 // Loan: $1B principal, 3 payments, 600s interval, rate=1 TenthBips32.
8065 auto const loanSetFee = Fee(env.current()->fees().base * 2);
8066 Number const principalRequest{1, 9};
8067 auto createJson = env.json(
8068 set(borrower, broker.brokerID, principalRequest),
8069 Fee(loanSetFee),
8070 Json(sfCounterpartySignature, json::ValueType::Object));
8071 createJson["InterestRate"] = 1;
8072 createJson["PaymentTotal"] = 3;
8073 createJson["PaymentInterval"] = 600;
8074
8075 auto const brokerStateBefore = env.le(keylet::loanBroker(broker.brokerID));
8076 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
8077 auto const loanKeylet = keylet::loan(broker.brokerID, loanSequence);
8078 createJson = env.json(createJson, Sig(sfCounterpartySignature, lender));
8079 env(createJson, Ter(tesSUCCESS));
8080 env.close();
8081
8082 auto const loanSle = env.le(loanKeylet);
8083 BEAST_EXPECT(loanSle);
8084 Number const expectedTotalInterest =
8085 loanSle->at(sfTotalValueOutstanding) - loanSle->at(sfPrincipalOutstanding);
8086
8087 env(pay(borrower, loanKeylet.key, iouAsset(1'500'000'000LL)), Ter(tesSUCCESS));
8088 env.close();
8089
8090 auto const vaultAfter = env.le(broker.vaultKeylet());
8091 Number const vaultAvailableAfter = vaultAfter->at(sfAssetsAvailable);
8092 Number const vaultGain = vaultAvailableAfter - vaultAvailableBefore;
8093
8094 STAmount const borrowerEndBal = usdBalance(borrower);
8095 STAmount const borrowerNetOut = borrowerStartBal - borrowerEndBal;
8096
8097 // Self-consistency: vault gained exactly the expected interest
8098 // computed at LoanSet, and the borrower's outflow matches.
8099 BEAST_EXPECT(vaultGain == expectedTotalInterest);
8100 BEAST_EXPECT(Number(borrowerNetOut) == expectedTotalInterest);
8101
8102 // Mathematical correctness: the total interest for this loan
8103 // configuration is 0.38051750382930729983, calculated
8104 // independently using 50-digit Decimal arithmetic (no
8105 // cancellation possible at that precision). At Number's 19-digit
8106 // mantissa this rounds to 0.38051750382930729 — the literal
8107 // below. The vault's actual gain must agree to within
8108 // sub-microcent precision.
8109 Number const decimalReference{38051750382930729LL, -17};
8110 Number const tolerance{1, -6}; // 1e-6 USD = sub-microcent
8111 Number const error = abs(vaultGain - decimalReference);
8112 BEAST_EXPECTS(
8113 error < tolerance,
8114 "vault gain " + to_string(vaultGain) + " differs from Decimal reference " +
8115 to_string(decimalReference) + " by " + to_string(error) + " — exceeds tolerance " +
8116 to_string(tolerance));
8117 }
8118
8119 // Verify that LoanPay, LoanBrokerCoverWithdraw, and LoanSet all use the
8120 // same vault-scale minimum cover when fixCleanup3_2_0 is enabled.
8121 // Before the amendment, each transactor computed its minimum cover at a
8122 // different precision (loanScale, debtScale, or the raw unrounded
8123 // tenthBipsOfValue), which could lead to inconsistent decisions for the
8124 // same broker state. After the amendment all three use
8125 // minimumBrokerCover at vaultScale.
8126 void
8128 {
8129 using namespace jtx;
8130 using namespace loan;
8131 using namespace loanBroker;
8132
8133 bool const withAmendment = features[fixCleanup3_2_0];
8134
8135 struct Ctx
8136 {
8137 jtx::Account issuer;
8138 jtx::Account lender;
8139 jtx::Account borrower;
8140 jtx::PrettyAsset iou;
8141 BrokerInfo broker;
8142 BrokerParameters brokerParams;
8143 };
8144
8145 // Shared setup, parametrized by vaultDeposit (the only varying setup
8146 // field across the three scenarios). Each call runs in its own Env
8147 // so multiple invocations within one scenario cannot interfere.
8148 // The caller is responsible for invoking testcase(...) before the
8149 // first runTest call of each scenario.
8150 auto runTest = [&](Number vaultDeposit, auto&& body) {
8151 Env env(*this, features);
8152
8153 Account const issuer{"issuer"};
8154 Account const lender{"lender"};
8155 Account const borrower{"borrower"};
8156
8157 env.fund(XRP(1'000'000'000), issuer, lender, borrower);
8158 env.close();
8159
8160 // Enable clawback on the issuer *before* any trust lines exist
8161 // (asfAllowTrustLineClawback requires an empty owner directory).
8162 env(fset(issuer, asfAllowTrustLineClawback));
8163 env.close();
8164
8165 PrettyAsset const iou = issuer[iouCurrency_];
8166 env(trust(lender, iou(1'000'000'000)));
8167 env(trust(borrower, iou(1'000'000'000)));
8168 env.close();
8169 env(pay(issuer, lender, iou(100'000'000)));
8170 env(pay(issuer, borrower, iou(100'000'000)));
8171 env.close();
8172
8173 // 13.37% — non-round rate produces a messier minimum.
8174 BrokerParameters const brokerParams{
8175 .vaultDeposit = vaultDeposit,
8176 .debtMax = 0,
8177 .coverRateMin = TenthBips32{13'370},
8178 .coverDeposit = 5'000,
8179 .managementFeeRate = TenthBips16{500}};
8180
8181 BrokerInfo const broker = createVaultAndBroker(env, iou, lender, brokerParams);
8182
8183 body(
8184 env,
8185 Ctx{.issuer = issuer,
8186 .lender = lender,
8187 .borrower = borrower,
8188 .iou = iou,
8189 .broker = broker,
8190 .brokerParams = brokerParams});
8191 };
8192
8193 // Scenario 1 — LoanPay
8194 //
8195 // Verify that LoanPay's minimum cover check uses vault scale (not
8196 // loan scale). Before the amendment, different loans could produce
8197 // different fee routing decisions for the same broker-level state.
8198 // Small vault deposit => vaultScale = -12.
8199 testcase("LoanPay minimum cover scale consistency");
8200 {
8201 struct LoanKeylets
8202 {
8203 Keylet tiny;
8204 Keylet big;
8205 };
8206
8207 // Create the tiny + big loans and reduce cover via clawback so
8208 // that subsequent LoanPay calls hit the minimum-cover boundary.
8209 // Used by the two pay-and-check sub-tests below so each can run
8210 // in its own Env.
8211 auto setupLoansAndClawback = [&](Env& env, Ctx const& c) -> std::optional<LoanKeylets> {
8212 Asset const asset{c.iou};
8213
8214 // Create the TINY loan first (while vaultScale is still
8215 // small). principal 0.01, 0% interest, 1 payment =>
8216 // loanScale = vaultScale.
8217 auto const brokerSle1 = env.le(keylet::loanBroker(c.broker.brokerID));
8218 if (!BEAST_EXPECT(brokerSle1))
8219 return std::nullopt;
8220 auto const tinyLoanSeq = brokerSle1->at(sfLoanSequence);
8221 auto const tinyLoanKeylet = keylet::loan(c.broker.brokerID, tinyLoanSeq);
8222
8223 env(set(c.borrower, c.broker.brokerID, Number{1, -2}),
8224 Sig(sfCounterpartySignature, c.lender),
8225 kInterestRate(TenthBips32{0}),
8226 kPaymentTotal(1),
8227 kPaymentInterval(86400 * 365),
8228 Fee(XRP(10)));
8229 env.close();
8230
8231 // Create the BIG loan second. 100% annual interest over 20
8232 // payments pushes totalValueOutstanding high enough that
8233 // loanScale > vaultScale.
8234 auto const brokerSle2 = env.le(keylet::loanBroker(c.broker.brokerID));
8235 if (!BEAST_EXPECT(brokerSle2))
8236 return std::nullopt;
8237 auto const bigLoanSeq = brokerSle2->at(sfLoanSequence);
8238 auto const bigLoanKeylet = keylet::loan(c.broker.brokerID, bigLoanSeq);
8239
8240 env(set(c.borrower, c.broker.brokerID, Number{500}),
8241 Sig(sfCounterpartySignature, c.lender),
8242 kInterestRate(TenthBips32{100'000}),
8243 kPaymentTotal(20),
8244 kPaymentInterval(86400 * 365),
8245 Fee(XRP(10)));
8246 env.close();
8247
8248 // The tiny loan's scale is frozen at the vault's pre-big-loan
8249 // scale, so it is strictly smaller than the big loan's.
8250 // After the big loan is created the vault absorbs its value,
8251 // pushing vaultScale up to match bigLoanScale.
8252 auto const tinyLoanSle = env.le(tinyLoanKeylet);
8253 auto const bigLoanSle = env.le(bigLoanKeylet);
8254 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
8255 if (!BEAST_EXPECT(tinyLoanSle) || !BEAST_EXPECT(bigLoanSle) ||
8256 !BEAST_EXPECT(vaultSle))
8257 return std::nullopt;
8258 if (!BEAST_EXPECT(tinyLoanSle->at(sfLoanScale) == -12) ||
8259 !BEAST_EXPECT(bigLoanSle->at(sfLoanScale) == -11) ||
8260 !BEAST_EXPECT(getAssetsTotalScale(vaultSle) == -11))
8261 return std::nullopt;
8262
8263 // Use issuer clawback to reduce cover to the minimum the
8264 // clawback transactor allows. Compute the amount as
8265 // initialCover - expectedCoverAfter so we exercise the exact
8266 // clawback rather than relying on the transactor to clip
8267 // down.
8268 //
8269 // Before the amendment the clawback minimum is the
8270 // *unrounded* tenthBipsOfValue — strictly less than the
8271 // rounded-at-vaultScale minimum LoanPay uses for the big
8272 // loan. After the amendment both clawback and LoanPay use
8273 // the same rounded minimum (via minimumBrokerCover), so
8274 // cover lands exactly at that threshold.
8275 Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15}
8276 : Number{1330651855688458000, -15};
8277 Number const clawbackAmount =
8278 Number{c.brokerParams.coverDeposit} - expectedCoverAfter;
8279
8280 env(coverClawback(c.issuer),
8281 kLoanBrokerId(c.broker.brokerID),
8283 env.close();
8284
8285 auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
8286 if (!BEAST_EXPECT(brokerSle) ||
8287 !BEAST_EXPECT(brokerSle->at(sfCoverAvailable) == expectedCoverAfter))
8288 return std::nullopt;
8289
8290 return LoanKeylets{.tiny = tinyLoanKeylet, .big = bigLoanKeylet};
8291 };
8292
8293 // Pay one loan and report whether the fee went to the broker's
8294 // pseudo account (the fallback when cover < minimum) rather
8295 // than to the owner.
8296 auto feeGoesToPseudo = [&](Env& env, Ctx const& c, Keylet const& loanKeylet) -> bool {
8297 Asset const asset{c.iou};
8298 auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
8299 if (!BEAST_EXPECT(brokerSle))
8300 return false;
8301 auto const pseudoAcct = Account("pseudo", brokerSle->at(sfAccount));
8302 auto const pseudoBefore = env.balance(pseudoAcct, c.iou);
8303
8304 auto const payLoan = env.le(loanKeylet);
8305 if (!BEAST_EXPECT(payLoan))
8306 return false;
8307 auto const periodicPayment = payLoan->at(sfPeriodicPayment);
8308 auto const serviceFee = payLoan->at(sfLoanServiceFee);
8309 std::int32_t const loanScale = payLoan->at(sfLoanScale);
8310
8311 auto const payment = roundPeriodicPayment(asset, periodicPayment, loanScale);
8312 auto const payAmt = STAmount{asset, payment + serviceFee};
8313
8314 env(loan::pay(c.borrower, loanKeylet.key, payAmt), Fee(XRP(10)));
8315 env.close();
8316
8317 auto const pseudoAfter = env.balance(pseudoAcct, c.iou);
8318 return pseudoAfter.number() > pseudoBefore.number();
8319 };
8320
8321 // Pay the BIG loan in its own Env so its outcome cannot affect
8322 // the TINY-loan check. With the fix, LoanPay and clawback use
8323 // the same vaultScale minimum (cover == minAtVaultScale =>
8324 // fee to owner). Without the fix, LoanPay uses bigLoanScale=-11,
8325 // rounds up to a larger minimum than what clawback used =>
8326 // cover < min => fee to pseudo.
8327 runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
8328 auto const loans = setupLoansAndClawback(env, c);
8329 if (!loans)
8330 return;
8331 BEAST_EXPECT(feeGoesToPseudo(env, c, loans->big) == !withAmendment);
8332 });
8333
8334 // Pay the TINY loan in its own Env. Fee goes to the owner
8335 // either way:
8336 // - With the fix: LoanPay uses vaultScale=-11 (same as
8337 // clawback) => owner.
8338 // - Without the fix: LoanPay uses tinyLoanScale=-12, rounds
8339 // up at -12 (a no-op) => min == cover => owner.
8340 runTest(/*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
8341 auto const loans = setupLoansAndClawback(env, c);
8342 if (!loans)
8343 return;
8344 BEAST_EXPECT(!feeGoesToPseudo(env, c, loans->tiny));
8345 });
8346 }
8347
8348 // Scenario 2 — LoanBrokerCoverWithdraw
8349 //
8350 // Verify that CoverWithdraw's minimum cover check uses vault scale
8351 // (not scale(debtTotal, asset)). Before the amendment, CoverWithdraw
8352 // used:
8353 // roundToAsset(asset, tenthBipsOfValue(debt, rate), scale(debt, asset))
8354 // which could disagree with LoanPay's minimum (which used loanScale).
8355 //
8356 // Use a large vault deposit so that vaultScale (from AssetsTotal) is
8357 // strictly larger than debtScale (from DebtTotal). With
8358 // vaultDeposit = 100,000: after the big loan
8359 // AssetsTotal ≈ 109,500 → vaultScale = -10
8360 // DebtTotal ≈ 10,000 → debtScale = -11
8361 // The one-order-of-magnitude gap makes roundToAsset at -10 truncate
8362 // more aggressively than at -11, exposing the bug.
8363 testcase("CoverWithdraw minimum cover scale consistency");
8364 runTest(
8365 /*vaultDeposit=*/100'000, [&](Env& env, Ctx const& c) {
8366 Asset const asset{c.iou};
8367
8368 // Create only the big loan to push DebtTotal up to ~10,000
8369 // while AssetsTotal stays around 109,500 (dominated by the
8370 // large vault deposit).
8371 env(set(c.borrower, c.broker.brokerID, Number{500}),
8372 Sig(sfCounterpartySignature, c.lender),
8373 kInterestRate(TenthBips32{100'000}),
8374 kPaymentTotal(20),
8375 kPaymentInterval(86400 * 365),
8376 Fee(XRP(10)));
8377 env.close();
8378
8379 // Read broker state and compute both old and new minimums.
8380 auto const brokerSle = env.le(keylet::loanBroker(c.broker.brokerID));
8381 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
8382 if (!BEAST_EXPECT(brokerSle) || !BEAST_EXPECT(vaultSle))
8383 return;
8384
8385 auto const coverAvail = brokerSle->at(sfCoverAvailable);
8386 auto const debtTotal = brokerSle->at(sfDebtTotal);
8387 auto const vaultScale = getAssetsTotalScale(vaultSle);
8388 auto const debtScale = scale(debtTotal, asset);
8389
8390 // Sanity: debt scale differs from vault scale for this setup.
8391 BEAST_EXPECT(debtScale < vaultScale);
8392
8393 auto const oldMin = [&]() {
8395 return roundToAsset(
8396 asset,
8397 tenthBipsOfValue(debtTotal, TenthBips32{c.brokerParams.coverRateMin}),
8398 debtScale);
8399 }();
8400 auto const newMin = minimumBrokerCover(
8401 debtTotal, TenthBips32{c.brokerParams.coverRateMin}, vaultSle);
8402
8403 // The new (vaultScale) minimum must be strictly larger than
8404 // the old (debtScale) minimum — that is the gap the amendment
8405 // closes.
8406 Number const expectedNewMin{1330650518688500000, -15};
8407 Number const expectedOldMin{1330650518688472000, -15};
8408 BEAST_EXPECT(newMin == expectedNewMin);
8409 BEAST_EXPECT(oldMin == expectedOldMin);
8410
8411 // Try to withdraw so that remaining cover lands between the
8412 // two minimums: oldMin < target < newMin.
8413 auto const target = oldMin + (newMin - oldMin) / 2;
8414 auto const withdrawAmount = STAmount{asset, coverAvail - target};
8415
8416 if (withAmendment)
8417 {
8418 // CoverWithdraw now uses vaultScale: target < newMin
8419 // => FAILS.
8420 env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount),
8422 }
8423 else
8424 {
8425 // Old CoverWithdraw uses debtScale: target > oldMin
8426 // => SUCCEEDS.
8427 env(coverWithdraw(c.lender, c.broker.brokerID, withdrawAmount));
8428 }
8429 env.close();
8430 });
8431
8432 // Scenario 3 — LoanSet
8433 //
8434 // Verify that LoanSet's minimum cover check uses vault scale (not the
8435 // raw unrounded tenthBipsOfValue). Before the amendment, LoanSet
8436 // used tenthBipsOfValue(newDebtTotal, coverRateMinimum) (no
8437 // roundToAsset), while clawback/withdraw used different formulas.
8438 // After the amendment all use minimumBrokerCover at vaultScale, and
8439 // rounding at a coarser scale can absorb a tiny debt increase —
8440 // allowing a loan that would otherwise be rejected.
8441 testcase("LoanSet minimum cover scale consistency");
8442 runTest(
8443 /*vaultDeposit=*/1'000, [&](Env& env, Ctx const& c) {
8444 // Create the tiny loan (scale -12) AND the big loan (scale
8445 // -11). Both loans are needed so that DebtTotal has a full
8446 // 16-digit mantissa — a "messy" value where roundToAsset at
8447 // vaultScale actually truncates digits and produces a
8448 // different result from the raw tenthBipsOfValue. With only
8449 // the big loan, DebtTotal has ~4 significant digits and
8450 // rounding at scale -11 is a no-op, masking the amendment's
8451 // effect.
8452 env(set(c.borrower, c.broker.brokerID, Number{1, -2}),
8453 Sig(sfCounterpartySignature, c.lender),
8454 kInterestRate(TenthBips32{0}),
8455 kPaymentTotal(1),
8456 kPaymentInterval(86400 * 365),
8457 Fee(XRP(10)));
8458 env.close();
8459
8460 env(set(c.borrower, c.broker.brokerID, Number{500}),
8461 Sig(sfCounterpartySignature, c.lender),
8462 kInterestRate(TenthBips32{100'000}),
8463 kPaymentTotal(20),
8464 kPaymentInterval(86400 * 365),
8465 Fee(XRP(10)));
8466 env.close();
8467
8468 // Clawback to reduce cover to the clawback transactor's
8469 // minimum. Pass the exact amount rather than relying on the
8470 // transactor to clip down; the setup matches Scenario 1 so
8471 // the same residual-cover values apply.
8472 Number const expectedCoverAfter = withAmendment ? Number{1330651855688460000, -15}
8473 : Number{1330651855688458000, -15};
8474 Number const clawbackAmount =
8475 Number{c.brokerParams.coverDeposit} - expectedCoverAfter;
8476 env(coverClawback(c.issuer),
8477 kLoanBrokerId(c.broker.brokerID),
8478 kAmount(c.iou(clawbackAmount)));
8479 env.close();
8480
8481 // Verify scales.
8482 auto const vaultSle = env.le(keylet::vault(c.broker.vaultID));
8483 if (!BEAST_EXPECT(vaultSle))
8484 return;
8485 auto const vaultScale = getAssetsTotalScale(vaultSle);
8486 BEAST_EXPECT(vaultScale == -11);
8487
8488 // Now try to create a tiny additional loan. Principal is
8489 // 1e-11 (the smallest value that survives the precision
8490 // check at loanScale = vaultScale = -11), with 0% interest
8491 // and 1 payment.
8492 //
8493 // The tiny debt increase adds ~1.337e-12 to the unrounded
8494 // minimum.
8495 // - Without the amendment: the old LoanSet formula rounds
8496 // up during tenthBipsOfValue (16-digit Number
8497 // normalisation), pushing the minimum past the cover left
8498 // by clawback => tecINSUFFICIENT_FUNDS.
8499 // - With the amendment: minimumBrokerCover rounds at
8500 // vaultScale=-11, which absorbs the tiny increase — the
8501 // rounded minimum stays the same => tesSUCCESS.
8502 auto const tinyPrincipal = Number{1, -11};
8503
8504 if (withAmendment)
8505 {
8506 env(set(c.borrower, c.broker.brokerID, tinyPrincipal),
8507 Sig(sfCounterpartySignature, c.lender),
8508 kInterestRate(TenthBips32{0}),
8509 kPaymentTotal(1),
8510 kPaymentInterval(86400 * 365),
8511 Fee(XRP(10)));
8512 }
8513 else
8514 {
8515 env(set(c.borrower, c.broker.brokerID, tinyPrincipal),
8516 Sig(sfCounterpartySignature, c.lender),
8517 kInterestRate(TenthBips32{0}),
8518 kPaymentTotal(1),
8519 kPaymentInterval(86400 * 365),
8520 Fee(XRP(10)),
8522 }
8523 env.close();
8524 });
8525 }
8526
8527 void
8551
8552 // Tests run under each entry in amendmentCombinations().
8553 void
8555 {
8556#if LOAN_TODO
8557 testLoanPayLateFullPaymentBypassesPenalties(features);
8558 testLoanCoverMinimumRoundingExploit(features);
8559#endif
8560 // Lifecycle
8561 testLifecycle(features);
8562 testLoanSet(features);
8563 testDosLoanPay(features);
8564 testSelfLoan(features);
8565
8566 // Payment paths
8572
8573 // Invariants
8577 testWrongMaxDebtBehavior(features);
8581
8582 // RPC
8583 testRPC(features);
8584
8585 // Edge / rounding
8586 testDustManipulation(features);
8589 testIssuerIsBorrower(features);
8592
8593 // RIPD regressions
8594 testRIPD3831(features);
8595 testRIPD3459(features);
8596 testRIPD3902(features);
8597
8598 // Broker-owner permissions
8603 }
8604
8605public:
8606 void
8607 run() override
8608 {
8610 for (auto const& features : jtx::amendmentCombinations(
8611 {fixCleanup3_1_3, fixCleanup3_2_0, featureMPTokensV2}, all_))
8612 runAmendmentSensitive(features);
8613 }
8614};
8615
8617{
8618protected:
8620
8628 /*
8629 # Generate parameters that are more likely to be valid
8630 principal = Decimal(str(rand.randint(100000,
8631 100'000'000))).quantize(ROUND_TARGET)
8632
8633 interest_rate = Decimal(rand.randint(1, 10000)) /
8634 Decimal(100000)
8635
8636 payment_total = rand.randint(12, 10000)
8637
8638 payment_interval = Decimal(str(rand.randint(60, 2629746)))
8639
8640 interest_fee = Decimal(rand.randint(0, 100000)) /
8641 Decimal(100000)
8642*/
8643
8644 void
8646 {
8647 using namespace jtx;
8648
8649 Account const issuer("issuer");
8650 Account const lender("lender");
8651 Account const borrower("borrower");
8652
8653 // Determine all the random parameters at once
8654 AssetType const assetType = static_cast<AssetType>(assetDist_(engine_));
8655 auto const principalRequest = principalDist_(engine_);
8656 TenthBips16 const managementFeeRate{managementFeeRateDist_(engine_)};
8657 auto const serviceFee = serviceFeeDist_(engine_);
8659 auto const payTotal = paymentTotalDist_(engine_);
8660 auto const payInterval = paymentIntervalDist_(engine_);
8661
8662 BrokerParameters const brokerParams{
8663 .vaultDeposit = principalRequest * 10,
8664 .debtMax = 0,
8665 .coverRateMin = TenthBips32{0},
8666 .managementFeeRate = managementFeeRate};
8667 LoanParameters const loanParams{
8668 .account = lender,
8669 .counter = borrower,
8670 .principalRequest = principalRequest,
8671 .serviceFee = serviceFee,
8672 .interest = interest,
8673 .payTotal = payTotal,
8674 .payInterval = payInterval,
8675 };
8676
8677 runLoan(assetType, brokerParams, loanParams, all_);
8678 }
8679
8680public:
8681 void
8682 run() override
8683 {
8684 auto const numIterations = [s = arg()]() -> int {
8685 int const defaultNum = 5;
8686 if (s.empty())
8687 return defaultNum;
8688 try
8689 {
8690 std::size_t pos = 0;
8691 auto const r = stoi(s, &pos);
8692 if (pos != s.size())
8693 return defaultNum;
8694 return r;
8695 }
8696 catch (...)
8697 {
8698 return defaultNum;
8699 }
8700 }();
8701
8702 using namespace jtx;
8703
8704 auto const updateInterval = std::min(numIterations / 5, 100);
8705
8706 for (int i = 0; i < numIterations; ++i)
8707 {
8708 if (i % updateInterval == 0)
8709 testcase << "Random Loan Test iteration " << (i + 1) << "/" << numIterations;
8711 }
8712 }
8713};
8716{
8717 void
8718 run() override
8719 {
8720 using namespace jtx;
8721
8722 BrokerParameters const brokerParams{
8723 .vaultDeposit = 10000,
8724 .debtMax = 0,
8725 .coverRateMin = TenthBips32{0},
8726 .managementFeeRate = TenthBips16{0},
8727 .coverRateLiquidation = TenthBips32{0}};
8728 LoanParameters const loanParams{
8729 .account = Account("lender"),
8730 .counter = Account("borrower"),
8731 .principalRequest = Number{200000, -6},
8732 .interest = TenthBips32{50000},
8733 .payTotal = 2,
8734 .payInterval = 200};
8735
8736 runLoan(AssetType::XRP, brokerParams, loanParams, all_);
8737 }
8738};
8742BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx, xrpl);
8743
8744} // namespace xrpl::test
T any_of(T... args)
A testsuite class.
Definition suite.h:50
LogOs< char > log
Logging output stream.
Definition suite.h:146
TestcaseT testcase
Memberspace for declaring test cases.
Definition suite.h:149
std::string const & arg() const
Return the argument associated with the runner.
Definition suite.h:278
Lightweight wrapper to tag static string.
Definition json_value.h:44
Represents a JSON value.
Definition json_value.h:130
Value removeMember(char const *key)
Remove and return the named member.
constexpr TIss const & get() const
constexpr bool native() const
Definition Asset.h:115
constexpr bool holds() const
Definition Asset.h:166
static constexpr auto kDisabledTxTypes
A currency issued by an account.
Definition Issue.h:13
static constexpr std::uint32_t kDefaultPaymentTotal
static constexpr std::uint32_t kMinPaymentTotal
static constexpr std::uint32_t kDefaultPaymentInterval
static constexpr std::uint32_t kMinPaymentInterval
static constexpr std::uint32_t kDefaultGracePeriod
std::chrono::time_point< NetClock > time_point
Definition chrono.h:46
std::chrono::duration< rep, period > duration
Definition chrono.h:45
Number is a floating point type that can represent a wide range of values.
Definition Number.h:306
Number truncate() const noexcept
Definition Number.cpp:1062
constexpr int exponent() const noexcept
Returns the exponent of the external view of the Number.
Definition Number.h:661
Slice slice() const noexcept
Definition PublicKey.h:103
void negate()
Definition STAmount.h:568
Asset const & asset() const
Definition STAmount.h:478
json::Value getJson(JsonOptions=JsonOptions::Values::None) const override
Definition STAmount.cpp:734
STAmount const & value() const noexcept
Definition STAmount.h:592
Blob getFieldVL(SField const &field) const
Definition STObject.cpp:639
Serializer getSerializer() const
Definition STObject.h:994
void setFieldObject(SField const &field, STObject const &v)
Definition STObject.cpp:829
STObject getFieldObject(SField const &field) const
Definition STObject.cpp:668
Slice slice() const noexcept
Definition Serializer.h:44
virtual LoadFeeTrack & getFeeTrack()=0
An immutable linear range of bytes.
Definition Slice.h:26
void run() override
Runs the suite.
beast::xor_shift_engine engine_
std::uniform_int_distribution paymentIntervalDist_
std::uniform_int_distribution paymentTotalDist_
std::uniform_int_distribution< std::int64_t > principalDist_
void run() override
Runs the suite.
std::uniform_int_distribution< std::uint16_t > managementFeeRateDist_
std::uniform_int_distribution serviceFeeDist_
std::uniform_int_distribution< std::uint32_t > interestRateDist_
std::uniform_int_distribution assetDist_
void testWrongMaxDebtBehavior(FeatureBitset features)
void testCaseWrapper(jtx::Env &env, jtx::MPTTester &mptt, std::array< TAsset, NAsset > const &assets, BrokerInfo const &broker, Number const &loanAmount, int interestExponent)
Wrapper to run a series of lifecycle tests for a given asset and loan amount.
void describeLoan(jtx::Env &env, BrokerParameters const &brokerParams, LoanParameters const &loanParams, AssetType assetType, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
void testDosLoanPay(FeatureBitset features)
void testRIPD3902(FeatureBitset features)
void runAmendmentSensitive(FeatureBitset features)
void runLoan(AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, FeatureBitset features)
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)
void testBugOverpaymentPrincipalChange()
void testRIPD3831(FeatureBitset features)
void testBatchBypassCounterparty(FeatureBitset features)
void testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(FeatureBitset features)
void run() override
Runs the suite.
FeatureBitset const all_
Definition Loan_test.cpp:94
void testRPC(FeatureBitset features)
void testLoanPayComputePeriodicPaymentValidRateInvariant(FeatureBitset features)
bool canImpairLoan(jtx::Env const &env, BrokerInfo const &broker, LoanState const &state)
void lifecycle(std::string const &caseLabel, char const *label, jtx::Env &env, Number const &loanAmount, int interestExponent, jtx::Account const &lender, jtx::Account const &borrower, jtx::Account const &evan, BrokerInfo const &broker, jtx::Account const &pseudoAcct, std::uint32_t flags, std::function< void(Keylet const &loanKeylet, VerifyLoanStatus const &verifyLoanStatus)> toEndOfLife)
Runs through the complete lifecycle of a loan.
static std::string getCurrencyLabel(Asset const &asset)
void testLoanPayDebtDecreaseInvariant(FeatureBitset features)
void testBugInterestDueDeltaCrash()
void testWithdrawReflectsUnrealizedLoss(FeatureBitset features)
void testYieldTheftRounding(std::uint32_t flags)
void testLoanNextPaymentDueDateOverflow(FeatureBitset features)
void testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features)
void testLoanSetNearZeroInterestRateSucceeds()
void testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
void testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(FeatureBitset features)
void testFullLifecycleVaultPnLNearZeroRate()
void testRoundingAllowsUndercoverage(FeatureBitset features)
void testLoanSet(FeatureBitset features)
LoanState getCurrentState(jtx::Env const &env, BrokerInfo const &broker, Keylet const &loanKeylet)
Get the state without checking anything.
void makeLoanPayments(jtx::Env &env, BrokerInfo const &broker, LoanParameters const &loanParams, Keylet const &loanKeylet, VerifyLoanStatus const &verifyLoanStatus, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower, PaymentParameters const &paymentParams=PaymentParameters::defaults())
void testRIPD3459(FeatureBitset features)
void testAccountSendMptMinAmountInvariant(FeatureBitset features)
void testMinimumBrokerCoverConsistency(FeatureBitset features)
void testLifecycle(FeatureBitset features)
void testSelfLoan(FeatureBitset features)
void testSequentialFLCDepletion(FeatureBitset features)
void testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const &params=BrokerParameters::defaults())
void testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features)
void testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(FeatureBitset features)
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)
void testIssuerIsBorrower(FeatureBitset features)
void testDustManipulation(FeatureBitset features)
std::optional< std::tuple< BrokerInfo, Keylet, jtx::Account > > createLoan(jtx::Env &env, AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams, jtx::Account const &issuer, jtx::Account const &lender, jtx::Account const &borrower)
void testBugOverpayUnroundedAmount()
void testIntegerScalePrincipalSticks(FeatureBitset features)
void testLendingCanTradeDisabledNoImpact()
std::string const iouCurrency_
Definition Loan_test.cpp:95
void testOverpaymentManagementFee(FeatureBitset features)
void testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features)
void testServiceFeeOnBrokerDeepFreeze()
Immutable cryptographic account descriptor.
Definition jtx/Account.h:17
SecretKey const & sk() const
Return the secret key.
Definition jtx/Account.h:75
std::string const & human() const
Returns the human readable public key.
Definition jtx/Account.h:92
std::string const & name() const
Return the name.
Definition jtx/Account.h:61
PublicKey const & pk() const
Return the public key.
Definition jtx/Account.h:68
AccountID id() const
Returns the Account ID.
Definition jtx/Account.h:85
A transaction testing environment.
Definition Env.h:143
Application & app()
Definition Env.h:280
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
Definition Env.cpp:133
TER ter() const
Return the TER for the last JTx.
Definition Env.h:675
json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
Definition Env.h:592
SLE::const_pointer le(Account const &account) const
Return an account root.
Definition Env.cpp:284
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:296
void enableFeature(uint256 const feature)
Definition Env.cpp:682
void disableFeature(uint256 const feature)
Definition Env.cpp:690
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
Definition Env.cpp:275
Account const & master
Definition Env.h:147
json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
Definition Env.h:864
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
Definition Env.h:566
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Definition Env.cpp:201
beast::unit_test::Suite & test
Definition Env.h:145
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Definition Env.cpp:327
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
Definition Env.cpp:511
beast::Journal const journal
Definition Env.h:184
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
Definition Env.h:353
NetClock::time_point now()
Returns the current network time.
Definition Env.h:305
Set the fee on a JTx.
Definition fee.h:15
Converts to IOU Issue or STAmount.
Inject raw JSON.
Definition jtx_json.h:11
Test helper for creating, mutating, and asserting MPT and confidential MPT ledger state.
Definition mpt.h:385
void set(MPTSet const &set={})
Definition mpt.cpp:482
void create(MPTCreate const &arg=MPTCreate{})
Definition mpt.cpp:256
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
Definition mpt.cpp:368
MPTID const & issuanceID() const
Definition mpt.h:576
Converts to MPT Issue or STAmount.
Set a multisignature on a JTx.
Definition multisign.h:39
Set the regular signature on a JTx.
Definition sig.h:13
Set the expected result code for a JTx The test will fail if the code doesn't match.
Definition ter.h:13
Set the flags on a JTx.
Definition txflags.h:9
Adds an inner Batch transaction to a JTx and autofills it.
Definition batch.h:58
T duration_cast(T... args)
T emplace_back(T... args)
T endl(T... args)
T is_same_v
T log(T... args)
T make_pair(T... args)
T make_tuple(T... args)
T max(T... args)
T min(T... args)
detail::XorShiftEngine<> xor_shift_engine
XOR-shift Generator.
@ Object
object value (collection of name/value pairs).
Definition json_value.h:26
STL namespace.
Number loanPrincipalFromPeriodicPayment(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t paymentsRemaining)
ExtendedPaymentComponents computeOverpaymentComponents(Rules const &rules, Asset const &asset, int32_t const loanScale, Number const &overpayment, TenthBips32 const overpaymentInterestRate, TenthBips32 const overpaymentFeeRate, TenthBips16 const managementFeeRate)
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:34
Keylet loanBroker(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:557
Keylet unchecked(uint256 const &key) noexcept
Any ledger entry.
Definition Indexes.cpp:351
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Definition Indexes.cpp:563
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Definition Indexes.cpp:551
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Definition Indexes.cpp:533
Keylet account(AccountID const &id) noexcept
AccountID root.
Definition Indexes.cpp:186
Keylet trustLine(AccountID const &id0, AccountID const &id1, Currency const &currency) noexcept
The index of a trust line for a given currency.
Definition Indexes.cpp:241
json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Build an outer Batch transaction JSON object.
Definition batch.cpp:53
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate the expected outer Batch transaction fee.
Definition batch.cpp:35
json::Value deleteCred(jtx::Account const &acc, jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:40
json::Value accept(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:29
json::Value create(jtx::Account const &subject, jtx::Account const &issuer, std::string_view credType)
Definition creds.cpp:16
Deposit preauthorize operations.
Definition deposit.h:7
json::Value set(AccountID const &account, uint256 const &vaultId, uint32_t flags)
json::Value coverWithdraw(AccountID const &account, uint256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value coverDeposit(AccountID const &account, uint256 const &brokerID, STAmount const &amount, uint32_t flags)
json::Value coverClawback(AccountID const &account, std::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)
json::Value manage(AccountID const &account, uint256 const &loanID, std::uint32_t flags)
auto const kOverpaymentInterestRate
json::Value pay(AccountID const &account, uint256 const &loanID, STAmount const &amount, std::uint32_t flags)
std::vector< Credential > Credentials
json::Value setTx(AccountID const &account, Credentials const &credentials, std::optional< uint256 > domain)
uint256 getNewDomain(std::shared_ptr< STObject const > const &meta)
static NoneT const kNone
Definition tags.h:9
auto const kMptDexFlags
Definition mpt.h:25
json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Definition pay.cpp:14
std::vector< FeatureBitset > amendmentCombinations(std::initializer_list< uint256 > features, FeatureBitset seed)
Returns all 2^N permutations of a seed FeatureBitset with each subset of the given features excluded.
Definition Env.h:107
XrpT const XRP
Converts to XRP Issue or STAmount.
Definition amount.cpp:92
json::Value noop(Account const &account)
The null transaction.
Definition noop.h:9
std::uint32_t ownerCount(Env const &env, Account const &account)
XRPAmount txFee(Env const &env, std::uint16_t n)
std::unique_ptr< Config > makeConfig(std::map< std::string, std::string > extraTxQ={}, std::map< std::string, std::string > extraVoting={})
static Number number(STAmount const &a)
Definition AMM.cpp:44
json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
Definition flags.h:102
FeatureBitset testableAmendments()
Definition Env.h:76
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:70
json::Value offer(Account const &account, STAmount const &takerPays, STAmount const &takerGets, std::uint32_t flags)
Create an offer.
Definition offer.cpp:14
json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Definition trust.cpp:18
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
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:142
BEAST_DEFINE_TESTSUITE(AMMClawback, app, xrpl)
BEAST_DEFINE_TESTSUITE_MANUAL(AMMCalc, app, xrpl)
STTx createTx(bool disabling, LedgerIndex seq, PublicKey const &txKey)
Create ttUNL_MODIFY Tx.
constexpr XRPAmount
Convert XRP to drops (integral types).
Definition TxTest.h:48
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
static constexpr Number kNumZero
Definition Number.h:612
@ telINSUF_FEE_P
Definition TER.h:41
@ telENV_RPC_FAILED
Definition TER.h:52
@ terNO_RIPPLE
Definition TER.h:216
@ terNO_ACCOUNT
Definition TER.h:209
bool set(T &target, std::string const &name, Section const &section)
Set a value from a configuration Section If the named value is not found or doesn't parse as a T,...
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Definition Issue.h:97
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
std::string strHex(FwdIt begin, FwdIt end)
Definition strHex.h:10
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
Definition Protocol.h:99
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
Definition Protocol.h:111
int getAssetsTotalScale(SLE::const_ref vaultSle)
constexpr XRPAmount kDropsPerXrp
Number of drops per 1 XRP.
Definition XRPAmount.h:240
int scale(Number const &number, Asset const &asset)
Get the scale of a Number for a given asset.
Definition STAmount.h:779
@ tefBAD_SIGNATURE
Definition TER.h:169
@ tefBAD_AUTH
Definition TER.h:159
@ tefPAST_SEQ
Definition TER.h:165
@ tefNOT_MULTI_SIGNING
Definition TER.h:171
Number minimumBrokerCover(Number const &debtTotal, TenthBips32 coverRateMinimum, SLE::const_ref vaultSle)
static bool authorized(Port const &port, std::map< std::string, std::string > const &h)
constexpr FlagValue tmfMPTSetCanTrade
Definition TxFlags.h:368
TenthBips< std::uint32_t > TenthBips32
Definition Units.h:439
static FunctionType fixed(Keylet const &keylet)
TenthBips< std::uint16_t > TenthBips16
Definition Units.h:438
boost::outcome_v2::result< T, std::error_code > Result
Definition b58_utils.h:17
std::string to_string(BaseUInt< Bits, Tag > const &a)
Definition base_uint.h:633
constexpr std::size_t kMaxDataPayloadLength
The maximum length of Data payload.
Definition Protocol.h:242
TER checkLoanGuards(Asset const &vaultAsset, Number const &principalRequested, bool expectInterest, std::uint32_t paymentTotal, LoanProperties const &properties, beast::Journal j)
STAmount roundToScale(STAmount const &value, std::int32_t scale, Number::RoundingMode rounding=Number::getround())
Round an arbitrary precision Amount to the precision of an STAmount that has a given exponent.
constexpr TenthBips32 kTenthBipsPerUnity(kBipsPerUnity.value() *10)
LedgerSpecificFlags
STAmount clawbackAmount(SLE::const_ref vault, std::optional< STAmount > const &maybeAmount, AccountID const &account)
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
Definition View.cpp:554
LoanState computeTheoreticalLoanState(Rules const &rules, Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
constexpr Number abs(Number x) noexcept
Definition Number.h:823
void roundToAsset(A const &asset, Number &value)
Round an arbitrary precision Number IN PLACE to the precision of a given Asset.
Definition STAmount.h:722
BaseUInt< 160, detail::AccountIDTag > AccountID
A 160-bit unsigned that uniquely identifies an account.
Definition AccountID.h:28
Number roundPeriodicPayment(Asset const &asset, Number const &periodicPayment, std::int32_t scale)
Ensure the periodic payment is always rounded consistently.
@ TxSign
inner transaction to sign
Definition HashPrefix.h:51
LoanState constructRoundedLoanState(SLE::const_ref loan)
@ temINVALID
Definition TER.h:96
@ temINVALID_FLAG
Definition TER.h:97
@ temDISABLED
Definition TER.h:100
@ temBAD_AMOUNT
Definition TER.h:75
@ temBAD_SIGNATURE
Definition TER.h:91
@ temINVALID_INNER_BATCH
Definition TER.h:129
@ temBAD_SIGNER
Definition TER.h:101
bool isTesSuccess(TER x) noexcept
Definition TER.h:663
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
Number computeManagementFee(Asset const &asset, Number const &interest, TenthBips32 managementFeeRate, std::int32_t scale)
TERSubset< CanCvtToTER > TER
Definition TER.h:634
constexpr FlagValue tmfMPTCanEnableCanTrade
Definition TxFlags.h:348
TER requireAuth(ReadView const &view, MPTIssue const &mptIssue, AccountID const &account, AuthType authType=AuthType::Legacy, std::uint8_t depth=0)
Check if the account lacks required authorization for MPT.
@ tecWRONG_ASSET
Definition TER.h:358
@ tecLOCKED
Definition TER.h:356
@ tecNO_ENTRY
Definition TER.h:304
@ tecNO_AUTH
Definition TER.h:298
@ tecTOO_SOON
Definition TER.h:316
@ tecFROZEN
Definition TER.h:301
@ tecINSUFFICIENT_FUNDS
Definition TER.h:323
@ tecEXPIRED
Definition TER.h:312
@ tecPRECISION_LOSS
Definition TER.h:361
@ tecINSUFFICIENT_RESERVE
Definition TER.h:305
@ tecKILLED
Definition TER.h:314
@ tecLIMIT_EXCEEDED
Definition TER.h:359
@ tecNO_PERMISSION
Definition TER.h:303
@ tecHAS_OBLIGATIONS
Definition TER.h:315
@ tecINSUFFICIENT_PAYMENT
Definition TER.h:325
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)
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
BaseUInt< 256 > uint256
Definition base_uint.h:562
@ tesSUCCESS
Definition TER.h:240
constexpr FlagValue tfFullyCanonicalSig
Definition TxFlags.h:42
Number computeFullPaymentInterest(Number const &theoreticalPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
T parse(T... args)
T reserve(T... args)
T size(T... args)
T str(T... args)
A pair of SHAMap key and LedgerEntryType.
Definition Keylet.h:19
uint256 key
Definition Keylet.h:20
This structure captures the parts of a loan state.
Number principalOutstanding
BrokerInfo(jtx::PrettyAsset const &asset, Keylet const &brokerKeylet, Keylet const &vaultKeylet, BrokerParameters p)
int vaultScale(jtx::Env const &env) const
Number maxCoveredLoanValue(Number const &currentDebt) const
std::optional< std::uint8_t > vaultScale
static BrokerParameters const & defaults()
std::optional< TenthBips32 > lateInterest
std::optional< STAmount > setFee
std::optional< Number > lateFee
std::optional< std::uint32_t > flags
std::optional< std::uint32_t > payTotal
std::optional< std::uint32_t > payInterval
std::optional< TenthBips32 > interest
std::optional< Number > closeFee
jtx::JTx operator()(jtx::Env &env, BrokerInfo const &broker, FN const &... fN) const
std::optional< Number > serviceFee
std::optional< TenthBips32 > overpaymentInterest
std::optional< std::uint32_t > gracePd
std::optional< TenthBips32 > overFee
std::optional< TenthBips32 > closeInterest
std::optional< Number > originationFee
NetClock::time_point startDate
std::uint32_t const paymentInterval
std::optional< Number > overpaymentExtra
static PaymentParameters const & defaults()
Helper class to compare the expected state of a loan and loan broker against the data in the ledger.
void checkPayment(std::int32_t loanScale, jtx::Account const &account, jtx::PrettyAmount const &balanceBefore, STAmount const &expectedPayment, jtx::PrettyAmount const &adjustment) const
void checkBroker(Number const &principalOutstanding, Number const &interestOwed, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, std::uint32_t ownerCount) const
Checks the expected broker state against the ledger.
VerifyLoanStatus(jtx::Env const &env, BrokerInfo const &broker, jtx::Account const &pseudo, Keylet const &keylet)
void operator()(std::uint32_t previousPaymentDate, std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, Number const &loanScale, Number const &totalValue, Number const &principalOutstanding, Number const &managementFeeOutstanding, Number const &periodicPayment, std::uint32_t flags) const
Checks both the loan and broker expect states against the ledger.
void operator()(LoanState const &state) const
Checks both the loan and broker expect states against the ledger.
Execution context for applying a JSON transaction.
Definition JTx.h:23
std::shared_ptr< STTx const > stx
Definition JTx.h:33
Represents an XRP, IOU, or MPT quantity This customizes the string conversion and supports XRP conver...
STAmount const & value() const
Set the sequence number on a JTx.
Definition seq.h:12
static json::Value withdraw(WithdrawArgs const &args)
Definition vault.cpp:64
static json::Value deposit(DepositArgs const &args)
Definition vault.cpp:53
T time_since_epoch(T... args)
T to_string(T... args)
T value_or(T... args)