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