1#include <xrpl/beast/unit_test/suite.h>
4#include <test/jtx/mpt.h>
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>
11#include <xrpl/beast/xor_shift_engine.h>
12#include <xrpl/protocol/SField.h>
24 featureSingleAssetVault | featureLendingProtocol};
38 Env env(*
this, features);
42 env.
fund(
XRP(10000), alice, bob);
46 using namespace std::chrono_literals;
71 failAll(
all - featureMPTokensV1);
72 failAll(
all - featureSingleAssetVault - featureLendingProtocol);
73 failAll(
all - featureSingleAssetVault);
74 failAll(
all - featureLendingProtocol);
95 return debtLimit - currentDebt;
117 Keylet const& brokerKeylet_,
118 Keylet const& vaultKeylet_,
173 template <
class... FN>
179 using namespace jtx::loan;
187 sig(sfCounterpartySignature,
counter)(env, jt);
192 counterparty(
counter)(env, jt);
194 loanOriginationFee(broker.
asset(*originationFee).number())(
197 loanServiceFee(broker.
asset(*serviceFee).number())(env, jt);
199 latePaymentFee(broker.
asset(*lateFee).number())(env, jt);
201 closePaymentFee(broker.
asset(*closeFee).number())(env, jt);
203 overpaymentFee (*
overFee)(env, jt);
217 gracePeriod (*
gracePd)(env, jt);
219 return env.
jt(jt, fN...);
282 Number const& principalOutstanding,
283 Number const& interestOwed,
291 env.
test.BEAST_EXPECT(brokerSle))
294 brokerSle->at(sfManagementFeeRate)};
295 auto const brokerDebt = brokerSle->at(sfDebtTotal);
296 auto const expectedDebt = principalOutstanding + interestOwed;
297 env.
test.BEAST_EXPECT(brokerDebt == expectedDebt);
300 brokerSle->at(sfCoverAvailable));
306 env.
test.BEAST_EXPECT(vaultSle))
309 "vaultPseudoAccount", vaultSle->at(sfAccount)};
311 vaultSle->at(sfAssetsAvailable) ==
319 auto const total = vaultSle->at(sfAssetsTotal);
320 auto const available = vaultSle->at(sfAssetsAvailable);
327 vaultSle->at(sfLossUnrealized) == 0);
341 auto const borrowerScale =
347 broker.
asset, expectedPayment + adjustment, borrowerScale)};
351 (balanceBefore - balanceChangeAmount),
366 Number const& principalOutstanding,
367 Number const& managementFeeOutstanding,
368 Number const& periodicPayment,
375 loan->at(sfPreviousPaymentDate) == previousPaymentDate);
377 loan->at(sfPaymentRemaining) == paymentRemaining);
379 loan->at(sfNextPaymentDueDate) == nextPaymentDate);
380 env.
test.BEAST_EXPECT(loan->at(sfLoanScale) == loanScale);
382 loan->at(sfTotalValueOutstanding) == totalValue);
384 loan->at(sfPrincipalOutstanding) == principalOutstanding);
386 loan->at(sfManagementFeeOutstanding) ==
387 managementFeeOutstanding);
389 loan->at(sfPeriodicPayment) == periodicPayment);
394 auto const interestRate =
TenthBips32{loan->at(sfInterestRate)};
395 auto const paymentInterval = loan->at(sfPaymentInterval);
397 principalOutstanding,
406 env.
test.BEAST_EXPECT(brokerSle))
410 env.
test.BEAST_EXPECT(vaultSle))
416 vaultSle->at(sfLossUnrealized) ==
417 totalValue - managementFeeOutstanding);
422 vaultSle->at(sfLossUnrealized) == 0);
457 auto const deposit = asset(params.vaultDeposit);
458 auto const debtMaximumValue = asset(params.debtMax).value();
459 auto const coverDepositValue = asset(params.coverDeposit).value();
461 auto const coverRateMinValue = params.coverRateMin;
463 auto [tx, vaultKeylet] =
464 vault.create({.owner = lender, .asset = asset});
467 BEAST_EXPECT(env.
le(vaultKeylet));
470 {.depositor = lender, .id = vaultKeylet.key, .amount = deposit}));
475 BEAST_EXPECT(vault->at(sfAssetsAvailable) == deposit.value());
480 using namespace loanBroker;
481 env(
set(lender, vaultKeylet.key, params.flags),
483 managementFeeRate(params.managementFeeRate),
484 debtMaximum(debtMaximumValue),
485 coverRateMinimum(coverRateMinValue),
486 coverRateLiquidation(
TenthBips32(params.coverRateLiquidation)));
488 if (coverDepositValue != beast::zero)
489 env(coverDeposit(lender, keylet.
key, coverDepositValue));
493 return {asset, keylet, vaultKeylet, params};
507 if (
auto loan = env.
le(loanKeylet); BEAST_EXPECT(loan))
511 .startDate = tp{d{loan->at(sfStartDate)}},
512 .nextPaymentDate = loan->at(sfNextPaymentDueDate),
513 .paymentRemaining = loan->at(sfPaymentRemaining),
514 .loanScale = loan->at(sfLoanScale),
515 .totalValue = loan->at(sfTotalValueOutstanding),
516 .principalOutstanding = loan->at(sfPrincipalOutstanding),
517 .managementFeeOutstanding =
518 loan->at(sfManagementFeeOutstanding),
519 .periodicPayment = loan->at(sfPeriodicPayment),
520 .flags = loan->at(sfFlags),
521 .paymentInterval = loan->at(sfPaymentInterval),
522 .interestRate =
TenthBips32{loan->at(sfInterestRate)},
537 using namespace std::chrono_literals;
542 BEAST_EXPECT(state.previousPaymentDate == 0);
543 BEAST_EXPECT(tp{d{state.nextPaymentDate}} == state.startDate + 600s);
544 BEAST_EXPECT(state.paymentRemaining == 12);
545 BEAST_EXPECT(state.principalOutstanding == broker.
asset(1000).value());
552 state.principalOutstanding.exponent())));
553 BEAST_EXPECT(state.paymentInterval == 600);
558 state.periodicPayment * state.paymentRemaining,
561 state.managementFeeOutstanding ==
564 state.totalValue - state.principalOutstanding,
568 verifyLoanStatus(state);
580 BEAST_EXPECT(brokerSle))
582 if (
auto const vaultSle =
584 BEAST_EXPECT(vaultSle))
587 auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) -
588 vaultSle->at(sfAssetsAvailable);
589 auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) +
592 if (unrealizedLoss > assetsUnavailable)
624 auto const limit = asset(
627 if (lender != issuer)
628 env(
trust(lender, limit));
629 if (borrower != issuer)
630 env(
trust(borrower, limit));
639 env.
current()->fees().accountReserve(10) * 10, issuer);
642 env.
current()->fees().accountReserve(10) * 10,
646 env.
current()->fees().accountReserve(10) * 10,
654 PrettyAsset const asset{mptt.issuanceID(), 10'000};
657 if (lender != issuer)
658 mptt.authorize({.account = lender});
659 if (borrower != issuer)
660 mptt.authorize({.account = borrower});
685 createAsset(env, assetType, brokerParams, issuer, lender, borrower);
688 auto const interval =
701 log <<
"Loan properties:\n"
702 <<
"\tPrincipal: " << principal <<
std::endl
703 <<
"\tInterest rate: " << interest <<
std::endl
704 <<
"\tPayment interval: " << interval <<
std::endl
705 <<
"\tManagement Fee Rate: " << feeRate <<
std::endl
706 <<
"\tTotal Payments: " << total <<
std::endl
707 <<
"\tPeriodic Payment: " << props.periodicPayment <<
std::endl
708 <<
"\tTotal Value: " << props.totalValueOutstanding <<
std::endl
709 <<
"\tManagement Fee: " << props.managementFeeOwedToBroker
711 <<
"\tLoan Scale: " << props.loanScale <<
std::endl
712 <<
"\tFirst payment principal: " << props.firstPaymentPrincipal
738 env.
fund(env.
current()->fees().accountReserve(10) * 10, issuer);
739 if (lender != issuer)
741 env.
current()->fees().accountReserve(10) * 10,
743 if (borrower != issuer && borrower != lender)
745 env.
current()->fees().accountReserve(10) * 10,
749 env, brokerParams, loanParams, assetType, issuer, lender, borrower);
753 createAsset(env, assetType, brokerParams, issuer, lender, borrower);
756 if (asset.
native() || lender != issuer)
769 if (!BEAST_EXPECT(brokerSle))
771 auto const brokerPseudo = brokerSle->at(sfAccount);
772 return Account(
"Broker pseudo-account", brokerPseudo);
776 Account const& pseudoAcct = *pseudoAcctOpt;
780 if (!BEAST_EXPECT(brokerSle))
784 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
788 auto const loanSequence = brokerSle->at(sfLoanSequence);
793 Keylet const& loanKeylet = *loanKeyletOpt;
795 env(loanParams(env, broker));
817 auto const borrowerBalance = env.
balance(borrower, broker.
asset);
819 auto const baseFee = env.
current()->fees().base;
827 env.
current()->fees().accountReserve(
829 : broker.
asset(15).number());
831 auto const shortage = totalNeeded - borrowerBalance.number();
833 if (shortage > beast::zero &&
855 using namespace jtx::loan;
856 using namespace std::chrono_literals;
862 bool const showStepBalances = paymentParams.showStepBalances;
866 auto const baseFee = env.
current()->fees().base;
871 verifyLoanStatus(state);
877 env, broker, issuer, borrower, state, loanParams.
serviceFee);
887 auto const periodicRate =
889 STAmount const roundedPeriodicPayment{
892 broker.
asset, state.periodicPayment, state.loanScale)};
894 if (!showStepBalances)
895 log << currencyLabel <<
" Payment components: "
896 <<
"Payments remaining, "
897 <<
"rawInterest, rawPrincipal, "
899 <<
"trackedValueDelta, trackedPrincipalDelta, "
900 "trackedInterestDelta, trackedMgmtFeeDelta, special"
905 roundedPeriodicPayment + serviceFee,
911 state.principalOutstanding,
912 state.managementFeeOutstanding);
915 state.periodicPayment,
917 state.paymentRemaining,
920 if (showStepBalances)
922 log << currencyLabel <<
" Starting loan balances: "
923 <<
"\n\tTotal value: "
924 << currentRoundedState.valueOutstanding <<
"\n\tPrincipal: "
925 << currentRoundedState.principalOutstanding
926 <<
"\n\tInterest: " << currentRoundedState.interestDue
927 <<
"\n\tMgmt fee: " << currentRoundedState.managementFeeDue
928 <<
"\n\tPayments remaining " << state.paymentRemaining
934 <<
" Loan starting state: " << state.paymentRemaining
935 <<
", " << raw.interestDue <<
", "
936 << raw.principalOutstanding <<
", " << raw.managementFeeDue
937 <<
", " << currentRoundedState.valueOutstanding <<
", "
938 << currentRoundedState.principalOutstanding <<
", "
939 << currentRoundedState.interestDue <<
", "
940 << currentRoundedState.managementFeeDue <<
std::endl;
946 auto const extraAmount = paymentParams.overpaymentExtra
947 ? broker.
asset(*paymentParams.overpaymentExtra).value()
949 broker.
asset(10).value(),
950 STAmount{broker.asset, totalDue / 20});
953 STAmount{broker.
asset, totalDue * paymentParams.overpaymentFactor} +
956 auto const borrowerInitialBalance =
958 auto const initialState = state;
961 .trackedPrincipalDelta = 0,
962 .trackedManagementFeeDelta = 0};
963 Number totalInterestPaid = 0;
968 state.periodicPayment,
970 state.paymentRemaining,
973 auto validateBorrowerBalance = [&]() {
974 if (borrower == issuer || !paymentParams.validateBalances)
976 auto const totalSpent =
977 (totalPaid.trackedValueDelta + totalFeesPaid +
982 borrowerInitialBalance - totalSpent);
986 auto truncate = [defaultRound](
989 auto const p = places.value_or(defaultRound);
992 auto const factor =
Number{1, p};
993 return (n * factor).
truncate() / factor;
995 while (state.paymentRemaining > 0)
997 validateBorrowerBalance();
1003 state.principalOutstanding,
1004 state.managementFeeOutstanding,
1005 state.periodicPayment,
1007 state.paymentRemaining,
1011 paymentComponents.trackedValueDelta <= roundedPeriodicPayment ||
1012 (paymentComponents.specialCase ==
1014 paymentComponents.trackedValueDelta >=
1015 roundedPeriodicPayment));
1017 paymentComponents.trackedValueDelta ==
1018 paymentComponents.trackedPrincipalDelta +
1019 paymentComponents.trackedInterestPart() +
1020 paymentComponents.trackedManagementFeeDelta);
1023 state.periodicPayment,
1025 state.paymentRemaining - 1,
1028 currentTrueState - nextTrueState;
1033 paymentComponents.specialCase ==
1035 deltas.
total() == state.periodicPayment ||
1037 (deltas.
total() - state.periodicPayment).exponent()) > 14);
1039 if (!showStepBalances)
1040 log << currencyLabel
1041 <<
" Payment components: " << state.paymentRemaining <<
", "
1045 << paymentComponents.trackedValueDelta <<
", "
1046 << paymentComponents.trackedPrincipalDelta <<
", "
1047 << paymentComponents.trackedInterestPart() <<
", "
1048 << paymentComponents.trackedManagementFeeDelta <<
", "
1049 << (paymentComponents.specialCase ==
1052 : paymentComponents.specialCase ==
1058 auto const totalDueAmount =
STAmount{
1059 broker.
asset, paymentComponents.trackedValueDelta + serviceFee};
1061 if (paymentParams.validateBalances)
1069 Number const diff = totalDue - totalDueAmount;
1071 paymentComponents.specialCase ==
1073 diff == beast::zero ||
1074 (diff > beast::zero &&
1076 (
static_cast<Number>(diff) < 3)) ||
1077 (state.loanScale - diff.
exponent() > 13))));
1080 paymentComponents.trackedPrincipalDelta >= beast::zero &&
1081 paymentComponents.trackedPrincipalDelta <=
1082 state.principalOutstanding);
1084 paymentComponents.specialCase !=
1086 paymentComponents.trackedPrincipalDelta ==
1087 state.principalOutstanding);
1090 auto const borrowerBalanceBeforePayment =
1098 paymentParams.flags));
1100 env.
close(d{state.paymentInterval / 2});
1102 if (paymentParams.validateBalances)
1108 adjustment = env.
current()->fees().base;
1115 borrowerBalanceBeforePayment,
1120 if (showStepBalances)
1122 auto const loanSle = env.
le(loanKeylet);
1123 if (!BEAST_EXPECT(loanSle))
1127 auto const errors = nextTrueState -
current;
1128 log << currencyLabel <<
" Loan balances: "
1129 <<
"\n\tAmount taken: "
1130 << paymentComponents.trackedValueDelta
1131 <<
"\n\tTotal value: " <<
current.valueOutstanding
1133 <<
", error: " << truncate(errors.total())
1134 <<
")\n\tPrincipal: " <<
current.principalOutstanding
1137 <<
", error: " << truncate(errors.principal)
1138 <<
")\n\tInterest: " <<
current.interestDue
1139 <<
" (true: " << truncate(nextTrueState.
interestDue)
1140 <<
", error: " << truncate(errors.interest)
1141 <<
")\n\tMgmt fee: " <<
current.managementFeeDue
1143 <<
", error: " << truncate(errors.managementFee)
1144 <<
")\n\tPayments remaining "
1145 << loanSle->at(sfPaymentRemaining) <<
std::endl;
1147 currentRoundedState =
current;
1150 --state.paymentRemaining;
1151 state.previousPaymentDate = state.nextPaymentDate;
1152 if (paymentComponents.specialCase ==
1155 state.paymentRemaining = 0;
1156 state.nextPaymentDate = 0;
1160 state.nextPaymentDate += state.paymentInterval;
1162 state.principalOutstanding -=
1163 paymentComponents.trackedPrincipalDelta;
1164 state.managementFeeOutstanding -=
1165 paymentComponents.trackedManagementFeeDelta;
1166 state.totalValue -= paymentComponents.trackedValueDelta;
1168 if (paymentParams.validateBalances)
1169 verifyLoanStatus(state);
1171 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
1172 totalPaid.trackedPrincipalDelta +=
1173 paymentComponents.trackedPrincipalDelta;
1174 totalPaid.trackedManagementFeeDelta +=
1175 paymentComponents.trackedManagementFeeDelta;
1176 totalInterestPaid += paymentComponents.trackedInterestPart();
1177 totalFeesPaid += serviceFee;
1178 ++totalPaymentsMade;
1180 currentTrueState = nextTrueState;
1182 validateBorrowerBalance();
1185 BEAST_EXPECT(state.paymentRemaining == 0);
1186 BEAST_EXPECT(state.principalOutstanding == 0);
1188 auto const initialInterestDue = initialState.totalValue -
1189 (initialState.principalOutstanding +
1190 initialState.managementFeeOutstanding);
1191 if (paymentParams.validateBalances)
1195 totalPaid.trackedValueDelta == initialState.totalValue);
1197 totalPaid.trackedPrincipalDelta ==
1198 initialState.principalOutstanding);
1200 totalPaid.trackedManagementFeeDelta ==
1201 initialState.managementFeeOutstanding);
1204 BEAST_EXPECT(totalInterestPaid == initialInterestDue);
1205 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
1208 if (showStepBalances)
1210 auto const loanSle = env.
le(loanKeylet);
1211 if (!BEAST_EXPECT(loanSle))
1214 log << currencyLabel <<
" Total amounts paid: "
1215 <<
"\n\tTotal value: " << totalPaid.trackedValueDelta
1216 <<
" (initial: " << truncate(initialState.totalValue)
1219 initialState.totalValue - totalPaid.trackedValueDelta)
1220 <<
")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta
1221 <<
" (initial: " << truncate(initialState.principalOutstanding)
1224 initialState.principalOutstanding -
1225 totalPaid.trackedPrincipalDelta)
1226 <<
")\n\tInterest: " << totalInterestPaid
1227 <<
" (initial: " << truncate(initialInterestDue) <<
", error: "
1228 << truncate(initialInterestDue - totalInterestPaid)
1229 <<
")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta
1231 << truncate(initialState.managementFeeOutstanding)
1234 initialState.managementFeeOutstanding -
1235 totalPaid.trackedManagementFeeDelta)
1236 <<
")\n\tTotal payments made: " << totalPaymentsMade
1247 using namespace jtx;
1249 Account const issuer(
"issuer");
1250 Account const lender(
"lender");
1251 Account const borrower(
"borrower");
1256 env, assetType, brokerParams, loanParams, issuer, lender, borrower);
1257 if (!BEAST_EXPECT(loanResult))
1292 Number const& loanAmount,
1293 int interestExponent,
1303 Keylet const& loanKeylet,
1306 auto const [keylet, loanSequence] = [&]() {
1308 if (!BEAST_EXPECT(brokerSle))
1314 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1318 auto const loanSequence = brokerSle->at(sfLoanSequence);
1324 env, broker, pseudoAcct, keylet);
1329 if (!BEAST_EXPECT(loanSequence != 0))
1332 testcase << caseLabel <<
" " << label;
1334 using namespace jtx;
1335 using namespace loan;
1336 using namespace std::chrono_literals;
1338 auto applyExponent = [interestExponent,
1341 while (interestExponent > 0)
1343 auto const oldValue = value;
1346 BEAST_EXPECT(value / 10 == oldValue);
1348 while (interestExponent < 0)
1350 auto const oldValue = value;
1353 BEAST_EXPECT(value * 10 == oldValue);
1358 auto const borrowerOwnerCount = env.
ownerCount(borrower);
1360 auto const loanSetFee = env.
current()->fees().base * 2;
1364 .counterpartyExplicit =
false,
1365 .principalRequest = loanAmount,
1366 .setFee = loanSetFee,
1367 .originationFee = 1,
1376 .overpaymentInterest =
1383 Number const principalRequestAmount =
1385 auto const originationFeeAmount =
1387 auto const serviceFeeAmount =
1389 auto const lateFeeAmount = broker.
asset(*loanParams.
lateFee).value();
1390 auto const closeFeeAmount = broker.
asset(*loanParams.
closeFee).value();
1392 auto const borrowerStartbalance = env.
balance(borrower, broker.
asset);
1394 auto createJtx = loanParams(env, broker);
1400 auto const startDate =
1401 env.
current()->header().parentCloseTime.time_since_epoch().count();
1404 BEAST_EXPECT(brokerSle))
1406 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1);
1414 adjustment = 2 * env.
current()->fees().base;
1419 borrowerStartbalance.
value() + principalRequestAmount -
1420 originationFeeAmount - adjustment.
value());
1427 if (
auto loan = env.
le(keylet); BEAST_EXPECT(loan))
1434 BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence);
1435 BEAST_EXPECT(loan->at(sfBorrower) == borrower.
id());
1436 BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.
brokerID);
1438 loan->at(sfLoanOriginationFee) == originationFeeAmount);
1439 BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount);
1440 BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount);
1441 BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount);
1442 BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.
overFee);
1443 BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.
interest);
1445 loan->at(sfLateInterestRate) == *loanParams.
lateInterest);
1447 loan->at(sfCloseInterestRate) == *loanParams.
closeInterest);
1449 loan->at(sfOverpaymentInterestRate) ==
1451 BEAST_EXPECT(loan->at(sfStartDate) == startDate);
1453 loan->at(sfPaymentInterval) == *loanParams.
payInterval);
1454 BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.
gracePd);
1455 BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0);
1457 loan->at(sfNextPaymentDueDate) ==
1459 BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.
payTotal);
1461 loan->at(sfLoanScale) >=
1466 principalRequestAmount.
exponent())));
1468 loan->at(sfPrincipalOutstanding) == principalRequestAmount);
1475 state.principalOutstanding,
1477 state.paymentInterval,
1478 state.paymentRemaining,
1487 loanProperties.totalValueOutstanding,
1488 principalRequestAmount,
1489 loanProperties.managementFeeOwedToBroker,
1490 loanProperties.periodicPayment,
1495 env(manage(lender, keylet.
key, 0));
1498 auto jt = manage(lender, keylet.
key, 0);
1499 jt.removeMember(sfFlags.getName());
1535 auto const nextDueDate = startDate + *loanParams.
payInterval;
1543 loanProperties.loanScale,
1544 loanProperties.totalValueOutstanding,
1545 principalRequestAmount,
1546 loanProperties.managementFeeOwedToBroker,
1547 loanProperties.periodicPayment,
1553 if (BEAST_EXPECT(toEndOfLife))
1554 toEndOfLife(keylet, verifyLoanStatus);
1558 if (
auto loan = env.le(keylet); BEAST_EXPECT(loan))
1560 BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
1561 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
1563 auto const borrowerStartingBalance =
1564 env.balance(borrower, broker.
asset);
1580 static unsigned deleteCounter = 0;
1581 auto const deleter = ++deleteCounter % 2 ? lender : borrower;
1582 env(del(deleter, keylet.
key));
1586 if (deleter == borrower)
1591 adjustment = env.current()->fees().base;
1599 env.balance(borrower, broker.
asset).value() ==
1600 borrowerStartingBalance.value() - adjustment);
1601 BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
1604 BEAST_EXPECT(brokerSle))
1606 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1628 template <
class TAsset, std::
size_t NAsset>
1635 Number const& loanAmount,
1636 int interestExponent)
1638 using namespace jtx;
1639 using namespace Lending;
1641 auto const& asset = broker.
asset.
raw();
1643 auto const caseLabel = [&]() {
1645 ss <<
"Lifecycle: " << loanAmount <<
" " << currencyLabel
1646 <<
" Scale interest to: " << interestExponent <<
" ";
1651 using namespace loan;
1652 using namespace std::chrono_literals;
1656 Account const issuer{
"issuer"};
1659 Account const lender{
"lender"};
1661 Account const borrower{
"borrower"};
1667 Number const principalRequest = broker.
asset(loanAmount).value();
1669 BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10);
1670 Number const maxCoveredLoanRequest =
1671 broker.
asset(maxCoveredLoanValue).value();
1672 Number const totalVaultRequest =
1674 Number const debtMaximumRequest =
1677 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
1679 auto const pseudoAcct = [&]() {
1681 if (!BEAST_EXPECT(brokerSle))
1683 auto const brokerPseudo = brokerSle->at(sfAccount);
1684 return Account(
"Broker pseudo-account", brokerPseudo);
1687 auto const baseFee = env.
current()->fees().base;
1693 sig(sfCounterpartySignature, lender),
1700 sig(sfCounterpartySignature, borrower),
1706 sig(sfCounterpartySignature, lender),
1714 sig(sfCounterpartySignature, borrower),
1715 overpaymentFee(maxOverpaymentFee),
1720 sig(sfCounterpartySignature, lender),
1721 overpaymentFee(maxOverpaymentFee + 1),
1727 sig(sfCounterpartySignature, borrower),
1728 interestRate(maxInterestRate),
1732 sig(sfCounterpartySignature, borrower),
1738 sig(sfCounterpartySignature, lender),
1739 interestRate(maxInterestRate + 1),
1744 sig(sfCounterpartySignature, lender),
1751 sig(sfCounterpartySignature, borrower),
1752 lateInterestRate(maxLateInterestRate),
1756 sig(sfCounterpartySignature, borrower),
1762 sig(sfCounterpartySignature, lender),
1763 lateInterestRate(maxLateInterestRate + 1),
1768 sig(sfCounterpartySignature, lender),
1775 sig(sfCounterpartySignature, borrower),
1776 closeInterestRate(maxCloseInterestRate),
1780 sig(sfCounterpartySignature, borrower),
1786 sig(sfCounterpartySignature, lender),
1787 closeInterestRate(maxCloseInterestRate + 1),
1791 sig(sfCounterpartySignature, lender),
1798 sig(sfCounterpartySignature, borrower),
1799 overpaymentInterestRate(maxOverpaymentInterestRate),
1803 sig(sfCounterpartySignature, borrower),
1809 sig(sfCounterpartySignature, lender),
1810 overpaymentInterestRate(maxOverpaymentInterestRate + 1),
1814 sig(sfCounterpartySignature, lender),
1821 sig(sfCounterpartySignature, borrower),
1827 sig(sfCounterpartySignature, lender),
1834 sig(sfCounterpartySignature, borrower),
1840 sig(sfCounterpartySignature, lender),
1847 sig(sfCounterpartySignature, borrower),
1854 sig(sfCounterpartySignature, lender),
1861 env(
set(borrower, broker.
brokerID, principalRequest),
1862 sig(sfCounterpartySignature, lender),
1865 env(
signers(lender, 2, {{evan, 1}, {borrower, 1}}));
1866 env(
signers(borrower, 2, {{evan, 1}, {lender, 1}}));
1867 env(
set(borrower, broker.
brokerID, principalRequest),
1868 counterparty(lender),
1870 msig(sfCounterpartySignature, evan, borrower),
1874 env(
set(borrower, broker.
brokerID, principalRequest),
1875 counterparty(lender),
1876 msig(alice, issuer),
1877 msig(sfCounterpartySignature, evan, borrower),
1881 env(
set(borrower, broker.
brokerID, principalRequest),
1882 counterparty(lender),
1884 msig(sfCounterpartySignature, alice, issuer),
1890 env(
set(borrower, broker.
brokerID, principalRequest),
1891 counterparty(lender),
1893 msig(sfCounterpartySignature, evan, borrower),
1898 env(
set(borrower, broker.
brokerID, principalRequest),
1899 sig(sfCounterpartySignature, evan),
1904 counterparty(borrower),
1905 sig(sfCounterpartySignature, borrower),
1909 env(
set(lender, badKeylet.key, principalRequest),
1910 sig(sfCounterpartySignature, evan),
1914 env(
set(lender, badKeylet.key, principalRequest),
1915 counterparty(borrower),
1916 sig(sfCounterpartySignature, borrower),
1920 env(
set(lender, broker.
brokerID, principalRequest),
1921 counterparty(alice),
1922 sig(sfCounterpartySignature, alice),
1927 env(
set(evan, broker.
brokerID, totalVaultRequest + 1),
1928 sig(sfCounterpartySignature, lender),
1934 env(
set(evan, broker.
brokerID, maxCoveredLoanRequest + 1),
1935 sig(sfCounterpartySignature, lender),
1944 if (!BEAST_EXPECT(brokerSle))
1947 auto const vaultPseudo = [&]() {
1948 auto const vaultSle =
1950 if (!BEAST_EXPECT(vaultSle))
1953 auto const vaultPseudo =
1954 Account(
"Vault pseudo-account", vaultSle->at(sfAccount));
1958 auto const [
freeze, deepfreeze, unfreeze, expectedResult] =
1976 auto deepfreeze = [&](
Account const& holder) {
1982 auto unfreeze = [&](
Account const& holder) {
1999 auto unfreeze = [&](
Account const& holder) {
2012 for (
auto const& account : {vaultPseudo, evan})
2018 env(
set(evan, broker.
brokerID, debtMaximumRequest),
2019 sig(sfCounterpartySignature, lender),
2021 ter(expectedResult));
2024 BEAST_EXPECT(unfreeze);
2030 env(
set(evan, broker.
brokerID, debtMaximumRequest + 1),
2031 sig(sfCounterpartySignature, lender),
2047 for (
auto const& account :
2056 deepfreeze(account);
2059 env(
set(evan, broker.
brokerID, debtMaximumRequest),
2060 sig(sfCounterpartySignature, lender),
2062 ter(expectedResult));
2065 BEAST_EXPECT(unfreeze);
2071 env(
set(evan, broker.
brokerID, debtMaximumRequest + 1),
2072 sig(sfCounterpartySignature, lender),
2082 auto coverAvailable =
2083 [&env,
this](
uint256 const& brokerID,
Number const& expected) {
2085 BEAST_EXPECT(brokerSle))
2087 auto const available = brokerSle->
at(sfCoverAvailable);
2093 auto getDefaultInfo = [&env,
this](
2096 if (
auto const brokerSle =
2098 BEAST_EXPECT(brokerSle))
2106 state.principalOutstanding.exponent())));
2113 brokerSle->at(sfDebtTotal),
2116 state.totalValue - state.managementFeeOutstanding),
2122 auto replenishCover = [&env, &coverAvailable](
2125 Number const& startingCoverAvailable,
2126 Number const& amountToBeCovered) {
2128 broker.
brokerID, startingCoverAvailable - amountToBeCovered);
2132 STAmount{broker.asset, amountToBeCovered}));
2133 coverAvailable(broker.
brokerID, startingCoverAvailable);
2138 bool impair =
true) {
2139 return [&, impair, baseFlag](
2140 Keylet const& loanKeylet,
2149 BEAST_EXPECT(state.flags == baseFlag);
2151 auto const& broker = verifyLoanStatus.
broker;
2152 auto const startingCoverAvailable = coverAvailable(
2167 state.nextPaymentDate =
2174 verifyLoanStatus(state);
2177 auto const nextDueDate = tp{d{state.nextPaymentDate}};
2186 env.
close(nextDueDate + 60s);
2188 auto const [amountToBeCovered, brokerAcct] =
2189 getDefaultInfo(state, broker);
2200 startingCoverAvailable,
2204 state.paymentRemaining = 0;
2205 state.totalValue = 0;
2206 state.principalOutstanding = 0;
2207 state.managementFeeOutstanding = 0;
2208 state.nextPaymentDate = 0;
2209 verifyLoanStatus(state);
2217 env(
pay(borrower, loanKeylet.key, broker.
asset(300)),
2222 auto singlePayment = [&](
Keylet const& loanKeylet,
2231 verifyLoanStatus(state);
2241 env(
pay(borrower, loanKeylet.
key, broker.
asset(-80), txFlags),
2245 env(
pay(evan, loanKeylet.
key, broker.
asset(80), txFlags),
2259 state.periodicPayment * Number{15, -1}},
2263 (
Number{15, -1} / loanPaymentsPerFeeIncrement + 1)}),
2271 broker.
asset(state.periodicPayment * 2),
2276 broker.
asset(state.periodicPayment * 2),
2281 broker.
asset(state.periodicPayment * 2),
2286 broker.
asset(state.periodicPayment * 2),
2291 auto const otherAsset = broker.
asset.
raw() == assets[0].raw()
2294 env(
pay(borrower, loanKeylet.
key, otherAsset(100), txFlags),
2307 auto const borrowerBalanceBeforePayment =
2310 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2313 auto const transactionAmount = payoffAmount + broker.
asset(10);
2319 (borrowerBalanceBeforePayment.number() * 2 /
2320 state.periodicPayment / loanPaymentsPerFeeIncrement +
2326 borrowerBalanceBeforePayment.number() * 2},
2332 baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)};
2333 env(
pay(borrower, loanKeylet.
key, transactionAmount, txFlags),
2344 adjustment = badFee + goodFee;
2347 state.paymentRemaining = 0;
2348 state.principalOutstanding = 0;
2349 state.totalValue = 0;
2350 state.managementFeeOutstanding = 0;
2351 state.previousPaymentDate = state.nextPaymentDate +
2352 state.paymentInterval * (numPayments - 1);
2353 state.nextPaymentDate = 0;
2354 verifyLoanStatus(state);
2356 verifyLoanStatus.checkPayment(
2359 borrowerBalanceBeforePayment,
2371 return [&, baseFlag](
2372 Keylet const& loanKeylet,
2373 VerifyLoanStatus
const& verifyLoanStatus) {
2378 env.
close(state.startDate + 20s);
2379 auto const loanAge = (env.
now() - state.startDate).count();
2380 BEAST_EXPECT(loanAge == 30);
2390 Number const interval = state.paymentInterval;
2391 auto const periodicRate =
2396 STAmount const principalOutstanding{
2397 broker.
asset, state.principalOutstanding};
2398 STAmount
const accruedInterest{
2400 state.principalOutstanding * periodicRate * loanAge /
2404 broker.
asset(Number(1141552511415525, -19)));
2405 STAmount
const prepaymentPenalty{
2406 broker.
asset, state.principalOutstanding * Number(36, -3)};
2407 BEAST_EXPECT(prepaymentPenalty == broker.
asset(36));
2410 principalOutstanding + accruedInterest + prepaymentPenalty +
2417 broker.
asset(Number(1040000114155251, -12)).
number(),
2423 payoffAmount > state.paymentRemaining *
2424 (state.periodicPayment + broker.
asset(2).value()));
2438 return [&, baseFlag](
2439 Keylet
const& loanKeylet,
2440 VerifyLoanStatus
const& verifyLoanStatus) {
2450 auto const startingPayments = state.paymentRemaining;
2451 auto const rawPayoff = startingPayments *
2452 (state.periodicPayment + broker.
asset(2).value());
2453 STAmount
const payoffAmount{broker.
asset, rawPayoff};
2456 broker.
asset(Number(1024014840139457, -12)));
2457 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2464 state.paymentRemaining,
2476 "Loan overpayment allowed - Impair and Default",
2490 "Loan overpayment prohibited - Impair and Default",
2500 defaultImmediately(0));
2504 "Loan overpayment allowed - Default without Impair",
2518 "Loan overpayment prohibited - Default without Impair",
2528 defaultImmediately(0,
false));
2532 "Loan overpayment prohibited - Pay off immediately",
2546 "Loan overpayment allowed - Pay off immediately",
2560 "Loan overpayment prohibited - Combine all payments",
2570 combineAllPayments(0));
2574 "Loan overpayment allowed - Combine all payments",
2588 "Loan overpayment prohibited - Make payments",
2598 [&](Keylet
const& loanKeylet,
2599 VerifyLoanStatus
const& verifyLoanStatus) {
2605 BEAST_EXPECT(state.flags == 0);
2608 verifyLoanStatus(state);
2610 env.
close(state.startDate + 20s);
2611 auto const loanAge = (env.
now() - state.startDate).
count();
2612 BEAST_EXPECT(loanAge == 30);
2622 Number
const interval = state.paymentInterval;
2623 auto const periodicRate =
2627 Number(2283105022831050, -21, Number::unchecked{}));
2628 STAmount
const roundedPeriodicPayment{
2631 broker.
asset, state.periodicPayment, state.loanScale)};
2634 << currencyLabel <<
" Payment components: "
2635 <<
"Payments remaining, rawInterest, rawPrincipal, "
2636 "rawMFee, trackedValueDelta, trackedPrincipalDelta, "
2637 "trackedInterestDelta, trackedMgmtFeeDelta, special";
2639 auto const serviceFee = broker.
asset(2);
2642 roundedPeriodicPayment ==
2651 roundedPeriodicPayment + serviceFee,
2666 state.periodicPayment,
2668 state.paymentRemaining,
2672 state.principalOutstanding,
2673 state.managementFeeOutstanding);
2676 <<
" Loan starting state: " << state.paymentRemaining
2677 <<
", " << raw.interestDue <<
", "
2678 << raw.principalOutstanding <<
", "
2679 << raw.managementFeeDue <<
", "
2680 << rounded.valueOutstanding <<
", "
2681 << rounded.principalOutstanding <<
", "
2682 << rounded.interestDue <<
", "
2683 << rounded.managementFeeDue;
2688 STAmount
const transactionAmount =
2689 STAmount{broker.
asset, totalDue} + broker.
asset(10);
2693 transactionAmount ==
2700 auto const initialState = state;
2701 detail::PaymentComponents totalPaid{
2702 .trackedValueDelta = 0,
2703 .trackedPrincipalDelta = 0,
2704 .trackedManagementFeeDelta = 0};
2705 Number totalInterestPaid = 0;
2709 state.periodicPayment,
2711 state.paymentRemaining,
2714 while (state.paymentRemaining > 0)
2717 auto const paymentComponents =
2722 state.principalOutstanding,
2723 state.managementFeeOutstanding,
2724 state.periodicPayment,
2726 state.paymentRemaining,
2730 paymentComponents.trackedValueDelta <=
2731 roundedPeriodicPayment);
2734 state.periodicPayment,
2736 state.paymentRemaining - 1,
2738 detail::LoanStateDeltas
const deltas =
2739 currentTrueState - nextTrueState;
2743 <<
" Payment components: " << state.paymentRemaining
2744 <<
", " << deltas.interest <<
", " << deltas.principal
2745 <<
", " << deltas.managementFee <<
", "
2746 << paymentComponents.trackedValueDelta <<
", "
2747 << paymentComponents.trackedPrincipalDelta <<
", "
2748 << paymentComponents.trackedInterestPart() <<
", "
2749 << paymentComponents.trackedManagementFeeDelta <<
", "
2750 << (paymentComponents.specialCase ==
2753 : paymentComponents.specialCase ==
2758 auto const totalDueAmount = STAmount{
2760 paymentComponents.trackedValueDelta +
2761 serviceFee.number()};
2769 Number
const diff = totalDue - totalDueAmount;
2771 paymentComponents.specialCase ==
2773 diff == beast::zero ||
2774 (diff > beast::zero &&
2776 (
static_cast<Number
>(diff) < 3)) ||
2777 (state.loanScale - diff.exponent() > 13))));
2780 paymentComponents.trackedValueDelta ==
2781 paymentComponents.trackedPrincipalDelta +
2782 paymentComponents.trackedInterestPart() +
2783 paymentComponents.trackedManagementFeeDelta);
2785 paymentComponents.trackedValueDelta <=
2786 roundedPeriodicPayment);
2789 state.paymentRemaining < 12 ||
2797 Number(8333228695260180, -14),
2802 paymentComponents.trackedPrincipalDelta >=
2804 paymentComponents.trackedPrincipalDelta <=
2805 state.principalOutstanding);
2807 paymentComponents.specialCase !=
2809 paymentComponents.trackedPrincipalDelta ==
2810 state.principalOutstanding);
2812 paymentComponents.specialCase ==
2814 (state.periodicPayment.exponent() -
2815 (deltas.principal + deltas.interest +
2816 deltas.managementFee - state.periodicPayment)
2819 auto const borrowerBalanceBeforePayment =
2829 env(
pay(borrower, loanKeylet.key, transactionAmount));
2834 PrettyAmount adjustment = broker.
asset(0);
2837 adjustment = env.
current()->fees().base;
2841 verifyLoanStatus.checkPayment(
2844 borrowerBalanceBeforePayment,
2848 --state.paymentRemaining;
2849 state.previousPaymentDate = state.nextPaymentDate;
2850 if (paymentComponents.specialCase ==
2853 state.paymentRemaining = 0;
2854 state.nextPaymentDate = 0;
2858 state.nextPaymentDate += state.paymentInterval;
2860 state.principalOutstanding -=
2861 paymentComponents.trackedPrincipalDelta;
2862 state.managementFeeOutstanding -=
2863 paymentComponents.trackedManagementFeeDelta;
2864 state.totalValue -= paymentComponents.trackedValueDelta;
2866 verifyLoanStatus(state);
2868 totalPaid.trackedValueDelta +=
2869 paymentComponents.trackedValueDelta;
2870 totalPaid.trackedPrincipalDelta +=
2871 paymentComponents.trackedPrincipalDelta;
2872 totalPaid.trackedManagementFeeDelta +=
2873 paymentComponents.trackedManagementFeeDelta;
2874 totalInterestPaid +=
2875 paymentComponents.trackedInterestPart();
2876 ++totalPaymentsMade;
2878 currentTrueState = nextTrueState;
2882 BEAST_EXPECT(state.paymentRemaining == 0);
2883 BEAST_EXPECT(state.principalOutstanding == 0);
2887 totalPaid.trackedValueDelta == initialState.totalValue);
2889 totalPaid.trackedPrincipalDelta ==
2890 initialState.principalOutstanding);
2892 totalPaid.trackedManagementFeeDelta ==
2893 initialState.managementFeeOutstanding);
2897 totalInterestPaid ==
2898 initialState.totalValue -
2899 (initialState.principalOutstanding +
2900 initialState.managementFeeOutstanding));
2902 totalPaymentsMade == initialState.paymentRemaining);
2923 if (!BEAST_EXPECT(timed))
2929 auto const start = clock_type::now();
2931 auto const duration = std::chrono::duration_cast<duration_type>(
2932 clock_type::now() - start);
2934 log << label <<
" took " << duration.count() <<
"ms" <<
std::endl;
2951 [&](Keylet
const& loanKeylet,
2952 VerifyLoanStatus
const& verifyLoanStatus) {
2955 using namespace loan;
2959 auto const serviceFee = broker.
asset(2).value();
2961 STAmount
const totalDue{
2965 state.periodicPayment + serviceFee,
2969 time(
"single payment", [&]() {
2970 env(
pay(borrower, loanKeylet.key, totalDue));
2975 auto const numPayments = (state.paymentRemaining - 2);
2976 STAmount
const bigPayment{broker.
asset, totalDue * numPayments};
2977 XRPAmount
const bigFee{
2978 baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)};
2979 time(
"ten payments", [&]() {
2980 env(
pay(borrower, loanKeylet.key, bigPayment), fee(bigFee));
2984 time(
"final payment", [&]() {
2989 totalDue + STAmount{broker.asset, 1}));
2996 "Loan overpayment allowed - Explicit overpayment",
3006 [&](Keylet
const& loanKeylet,
3007 VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
3011 "Loan overpayment prohibited - Late payment",
3021 [&](Keylet
const& loanKeylet,
3022 VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
3026 "Loan overpayment allowed - Late payment",
3036 [&](Keylet
const& loanKeylet,
3037 VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
3041 "Loan overpayment allowed - Late payment and overpayment",
3051 [&](Keylet
const& loanKeylet,
3052 VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
3060 using namespace jtx;
3062 Account const issuer{
"issuer"};
3063 Account const lender{
"lender"};
3064 Account const borrower{
"borrower"};
3069 bool authorizeBorrower =
false;
3070 int initialXRP = 1'000'000;
3073 auto const testCase =
3078 CaseArgs args = {}) {
3080 env.
fund(
XRP(args.initialXRP), issuer, lender, borrower);
3082 if (args.requireAuth)
3098 mptt.authorize({.account = lender});
3099 mptt.authorize({.account = borrower});
3101 if (args.requireAuth)
3103 mptt.authorize({.account = issuer, .holder = lender});
3104 if (args.authorizeBorrower)
3105 mptt.authorize({.account = issuer, .holder = borrower});
3109 env(
pay(issuer, lender, mptAsset(10'000'000)));
3114 env(
trust(lender, iouAsset(10'000'000)));
3115 env(
trust(borrower, iouAsset(10'000'000)));
3117 if (args.requireAuth)
3120 env(
pay(issuer, lender, iouAsset(10'000'000)));
3121 if (args.authorizeBorrower)
3124 env(
pay(issuer, borrower, iouAsset(10'000)));
3129 env(
pay(issuer, lender, iouAsset(10'000'000)));
3130 env(
pay(issuer, borrower, iouAsset(10'000)));
3137 for (
auto const& asset : assets)
3140 createVaultAndBroker(env, asset, lender));
3144 (mptTest)(env, brokers[0], mptt);
3146 (iouTest)(env, brokers[1]);
3151 using namespace loan;
3152 Number const principalRequest = broker.
asset(1'000).value();
3154 testcase(
"MPT issuer is borrower, issuer submits");
3155 env(
set(issuer, broker.
brokerID, principalRequest),
3156 counterparty(lender),
3157 sig(sfCounterpartySignature, lender),
3160 testcase(
"MPT issuer is borrower, lender submits");
3161 env(
set(lender, broker.
brokerID, principalRequest),
3162 counterparty(issuer),
3163 sig(sfCounterpartySignature, issuer),
3167 using namespace loan;
3168 Number const principalRequest = broker.
asset(1'000).value();
3170 testcase(
"IOU issuer is borrower, issuer submits");
3171 env(
set(issuer, broker.
brokerID, principalRequest),
3172 counterparty(lender),
3173 sig(sfCounterpartySignature, lender),
3176 testcase(
"IOU issuer is borrower, lender submits");
3177 env(
set(lender, broker.
brokerID, principalRequest),
3178 counterparty(issuer),
3179 sig(sfCounterpartySignature, issuer),
3182 CaseArgs{.requireAuth =
true});
3186 using namespace loan;
3187 Number const principalRequest = broker.
asset(1'000).value();
3189 testcase(
"MPT unauthorized borrower, borrower submits");
3190 env(
set(borrower, broker.
brokerID, principalRequest),
3191 counterparty(lender),
3192 sig(sfCounterpartySignature, lender),
3196 testcase(
"MPT unauthorized borrower, lender submits");
3197 env(
set(lender, broker.
brokerID, principalRequest),
3198 counterparty(borrower),
3199 sig(sfCounterpartySignature, borrower),
3204 using namespace loan;
3205 Number const principalRequest = broker.
asset(1'000).value();
3207 testcase(
"IOU unauthorized borrower, borrower submits");
3208 env(
set(borrower, broker.
brokerID, principalRequest),
3209 counterparty(lender),
3210 sig(sfCounterpartySignature, lender),
3214 testcase(
"IOU unauthorized borrower, lender submits");
3215 env(
set(lender, broker.
brokerID, principalRequest),
3216 counterparty(borrower),
3217 sig(sfCounterpartySignature, borrower),
3221 CaseArgs{.requireAuth =
true});
3226 env.
current()->fees().accountReserve(0).drops() /
3228 env.
current()->fees().increment.drops() /
3234 using namespace loan;
3235 Number const principalRequest = broker.
asset(1'000).value();
3238 "MPT authorized borrower, borrower submits, borrower has "
3244 auto const mptoken =
3246 auto const sleMPT1 = env.
le(mptoken);
3247 BEAST_EXPECT(sleMPT1 ==
nullptr);
3250 env(
noop(borrower),
fee(
XRP(acctReserve * 2 + incReserve * 2)));
3254 env(
set(borrower, broker.
brokerID, principalRequest),
3255 counterparty(lender),
3256 sig(sfCounterpartySignature, lender),
3258 ter{tecINSUFFICIENT_RESERVE});
3262 env(
pay(issuer, borrower,
XRP(incReserve)));
3264 env(
set(borrower, broker.
brokerID, principalRequest),
3265 counterparty(lender),
3266 sig(sfCounterpartySignature, lender),
3270 auto const sleMPT2 = env.
le(mptoken);
3271 BEAST_EXPECT(sleMPT2 !=
nullptr);
3274 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
3279 using namespace loan;
3280 Number const principalRequest = broker.
asset(1'000).value();
3283 "IOU authorized borrower, borrower submits, borrower has "
3289 env(
pay(borrower, issuer, broker.
asset(10'000)));
3291 auto const trustline =
3293 auto const sleLine1 = env.
le(trustline);
3294 BEAST_EXPECT(sleLine1 ==
nullptr);
3297 env(
noop(borrower),
fee(
XRP(acctReserve * 2 + incReserve * 2)));
3301 env(
set(borrower, broker.
brokerID, principalRequest),
3302 counterparty(lender),
3303 sig(sfCounterpartySignature, lender),
3305 ter{tecNO_LINE_INSUF_RESERVE});
3309 env(
pay(issuer, borrower,
XRP(incReserve)));
3311 env(
set(borrower, broker.
brokerID, principalRequest),
3312 counterparty(lender),
3313 sig(sfCounterpartySignature, lender),
3317 auto const sleLine2 = env.
le(trustline);
3318 BEAST_EXPECT(sleLine2 !=
nullptr);
3320 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
3324 using namespace loan;
3325 Number const principalRequest = broker.
asset(1'000).value();
3328 "MPT authorized borrower, borrower submits, lender has "
3331 auto const sleMPT1 = env.
le(mptoken);
3332 BEAST_EXPECT(sleMPT1 !=
nullptr);
3335 lender, issuer, broker.
asset(sleMPT1->at(sfMPTAmount))));
3341 auto const sleMPT2 = env.
le(mptoken);
3342 BEAST_EXPECT(sleMPT2 ==
nullptr);
3349 env(
set(borrower, broker.
brokerID, principalRequest),
3350 loanOriginationFee(broker.
asset(1).value()),
3351 counterparty(lender),
3352 sig(sfCounterpartySignature, lender),
3354 ter{tecINSUFFICIENT_RESERVE});
3358 env(
pay(issuer, lender,
XRP(incReserve)));
3360 env(
set(borrower, broker.
brokerID, principalRequest),
3361 loanOriginationFee(broker.
asset(1).value()),
3362 counterparty(lender),
3363 sig(sfCounterpartySignature, lender),
3367 auto const sleMPT3 = env.
le(mptoken);
3368 BEAST_EXPECT(sleMPT3 !=
nullptr);
3371 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
3376 using namespace loan;
3377 Number const principalRequest = broker.
asset(1'000).value();
3380 "IOU authorized borrower, borrower submits, lender has no "
3386 auto const trustline =
3388 auto const sleLine1 = env.
le(trustline);
3389 BEAST_EXPECT(sleLine1 !=
nullptr);
3394 broker.
asset(
abs(sleLine1->at(sfBalance).value()))));
3396 auto const sleLine2 = env.
le(trustline);
3397 BEAST_EXPECT(sleLine2 ==
nullptr);
3404 env(
set(borrower, broker.
brokerID, principalRequest),
3405 loanOriginationFee(broker.
asset(1).value()),
3406 counterparty(lender),
3407 sig(sfCounterpartySignature, lender),
3409 ter{tecNO_LINE_INSUF_RESERVE});
3413 env(
pay(issuer, lender,
XRP(incReserve)));
3415 env(
set(borrower, broker.
brokerID, principalRequest),
3416 loanOriginationFee(broker.
asset(1).value()),
3417 counterparty(lender),
3418 sig(sfCounterpartySignature, lender),
3422 auto const sleLine3 = env.
le(trustline);
3423 BEAST_EXPECT(sleLine3 !=
nullptr);
3425 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
3429 using namespace loan;
3430 Number const principalRequest = broker.
asset(1'000).value();
3432 testcase(
"MPT authorized borrower, unauthorized lender");
3434 auto const sleMPT1 = env.
le(mptoken);
3435 BEAST_EXPECT(sleMPT1 !=
nullptr);
3438 lender, issuer, broker.
asset(sleMPT1->at(sfMPTAmount))));
3444 auto const sleMPT2 = env.
le(mptoken);
3445 BEAST_EXPECT(sleMPT2 ==
nullptr);
3448 env(
set(borrower, broker.
brokerID, principalRequest),
3449 loanOriginationFee(broker.
asset(1).value()),
3450 counterparty(lender),
3451 sig(sfCounterpartySignature, lender),
3457 env(
set(borrower, broker.
brokerID, principalRequest),
3458 counterparty(lender),
3459 sig(sfCounterpartySignature, lender),
3464 auto const sleMPT3 = env.
le(mptoken);
3465 BEAST_EXPECT(sleMPT3 ==
nullptr);
3468 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3472 using namespace loan;
3473 Number const principalRequest = broker.
asset(1'000).value();
3475 testcase(
"MPT authorized borrower, borrower submits");
3476 env(
set(borrower, broker.
brokerID, principalRequest),
3477 counterparty(lender),
3478 sig(sfCounterpartySignature, lender),
3482 using namespace loan;
3483 Number const principalRequest = broker.
asset(1'000).value();
3485 testcase(
"IOU authorized borrower, borrower submits");
3486 env(
set(borrower, broker.
brokerID, principalRequest),
3487 counterparty(lender),
3488 sig(sfCounterpartySignature, lender),
3491 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3495 using namespace loan;
3496 Number const principalRequest = broker.
asset(1'000).value();
3498 testcase(
"MPT authorized borrower, lender submits");
3499 env(
set(lender, broker.
brokerID, principalRequest),
3500 counterparty(borrower),
3501 sig(sfCounterpartySignature, borrower),
3505 using namespace loan;
3506 Number const principalRequest = broker.
asset(1'000).value();
3508 testcase(
"IOU authorized borrower, lender submits");
3509 env(
set(lender, broker.
brokerID, principalRequest),
3510 counterparty(borrower),
3511 sig(sfCounterpartySignature, borrower),
3514 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3518 auto const msigSetup = [&](
Env& env,
Account const& account) {
3526 using namespace loan;
3527 msigSetup(env, lender);
3528 Number const principalRequest = broker.
asset(1'000).value();
3531 "MPT authorized borrower, borrower submits, lender "
3533 env(
set(borrower, broker.
brokerID, principalRequest),
3534 counterparty(lender),
3535 msig(sfCounterpartySignature, alice, bella),
3539 using namespace loan;
3540 msigSetup(env, lender);
3541 Number const principalRequest = broker.
asset(1'000).value();
3544 "IOU authorized borrower, borrower submits, lender "
3546 env(
set(borrower, broker.
brokerID, principalRequest),
3547 counterparty(lender),
3548 msig(sfCounterpartySignature, alice, bella),
3551 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3555 using namespace loan;
3556 msigSetup(env, borrower);
3557 Number const principalRequest = broker.
asset(1'000).value();
3560 "MPT authorized borrower, lender submits, borrower "
3562 env(
set(lender, broker.
brokerID, principalRequest),
3563 counterparty(borrower),
3564 msig(sfCounterpartySignature, alice, bella),
3568 using namespace loan;
3569 msigSetup(env, borrower);
3570 Number const principalRequest = broker.
asset(1'000).value();
3573 "IOU authorized borrower, lender submits, borrower "
3575 env(
set(lender, broker.
brokerID, principalRequest),
3576 counterparty(borrower),
3577 msig(sfCounterpartySignature, alice, bella),
3580 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3586 testcase(
"Lifecycle");
3587 using namespace jtx;
3593 Account const issuer{
"issuer"};
3596 Account const lender{
"lender"};
3598 Account const borrower{
"borrower"};
3612 env(
trust(lender, iouAsset(10'000'000)));
3613 env(
trust(borrower, iouAsset(10'000'000)));
3614 env(
trust(evan, iouAsset(10'000'000)));
3615 env(
pay(issuer, evan, iouAsset(1'000'000)));
3616 env(
pay(issuer, lender, iouAsset(10'000'000)));
3618 env(
pay(issuer, borrower, iouAsset(10'000)));
3625 PrettyAsset const mptAsset{mptt.issuanceID(), 100};
3626 mptt.authorize({.account = lender});
3627 mptt.authorize({.account = borrower});
3628 mptt.authorize({.account = evan});
3629 env(
pay(issuer, lender, mptAsset(10'000'000)));
3630 env(
pay(issuer, evan, mptAsset(1'000'000)));
3632 env(
pay(issuer, borrower, mptAsset(10'000)));
3635 std::array const assets{xrpAsset, mptAsset, iouAsset};
3639 for (
auto const& asset : assets)
3649 for (
auto const& broker : brokers)
3651 for (
int amountExponent = 3; amountExponent >= 3; --amountExponent)
3653 Number const loanAmount{1, amountExponent};
3654 for (
int interestExponent = 0; interestExponent >= 0;
3668 BEAST_EXPECT(brokerSle))
3670 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
3671 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == 0);
3673 auto const coverAvailable = brokerSle->at(sfCoverAvailable);
3681 BEAST_EXPECT(brokerSle && brokerSle->at(sfCoverAvailable) == 0);
3692 testcase <<
"Self Loan";
3694 using namespace jtx;
3695 using namespace std::chrono_literals;
3700 Account const issuer{
"issuer"};
3703 Account const lender{
"lender"};
3714 BrokerInfo broker{createVaultAndBroker(env, xrpAsset, lender)};
3716 using namespace loan;
3718 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
3719 Number const principalRequest{1, 3};
3723 auto createJson = env.
json(
3726 broker.
asset(principalRequest).value()),
3732 createJson = env.
json(
3736 if (
auto const jt = env.
jt(createJson); BEAST_EXPECT(jt.stx))
3742 BEAST_EXPECT(jr.isMember(jss::result));
3743 auto const jResult = jr[jss::result];
3744 BEAST_EXPECT(jResult[jss::error] ==
"invalidTransaction");
3746 jResult[jss::error_exception] ==
3747 "fails local checks: Transaction has bad signature.");
3752 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
3753 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
3754 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
3755 counterpartyJson[sfSigners] = createJson[sfSigners];
3758 createJson = env.
json(
3759 createJson,
json(sfCounterpartySignature, counterpartyJson));
3764 auto const startDate = env.
current()->header().parentCloseTime;
3768 auto const res = env.
rpc(
"account_objects", lender.
human());
3769 auto const objects = res[jss::result][jss::account_objects];
3772 BEAST_EXPECT(objects.size() == 4);
3773 for (
auto const&
object : objects)
3775 ++types[
object[sfLedgerEntryType].asString()];
3777 BEAST_EXPECT(types.
size() == 4);
3779 {
"MPToken",
"Vault",
"LoanBroker",
"Loan"})
3781 BEAST_EXPECT(types[type] == 1);
3784 auto const loanID = [&]() {
3786 params[jss::account] = lender.
human();
3787 params[jss::type] =
"Loan";
3790 auto const objects = res[jss::result][jss::account_objects];
3792 BEAST_EXPECT(objects.size() == 1);
3794 auto const loan = objects[0u];
3795 BEAST_EXPECT(loan[sfBorrower] == lender.
human());
3798 BEAST_EXPECT(!loan.isMember(sfCloseInterestRate));
3799 BEAST_EXPECT(!loan.isMember(sfClosePaymentFee));
3800 BEAST_EXPECT(loan[sfFlags] == 0);
3801 BEAST_EXPECT(loan[sfGracePeriod] == 60);
3802 BEAST_EXPECT(!loan.isMember(sfInterestRate));
3803 BEAST_EXPECT(!loan.isMember(sfLateInterestRate));
3804 BEAST_EXPECT(!loan.isMember(sfLatePaymentFee));
3806 BEAST_EXPECT(!loan.isMember(sfLoanOriginationFee));
3807 BEAST_EXPECT(loan[sfLoanSequence] == 1);
3808 BEAST_EXPECT(!loan.isMember(sfLoanServiceFee));
3810 loan[sfNextPaymentDueDate] == loan[sfStartDate].asUInt() + 60);
3811 BEAST_EXPECT(!loan.isMember(sfOverpaymentFee));
3812 BEAST_EXPECT(!loan.isMember(sfOverpaymentInterestRate));
3813 BEAST_EXPECT(loan[sfPaymentInterval] == 60);
3814 BEAST_EXPECT(loan[sfPeriodicPayment] ==
"1000000000");
3815 BEAST_EXPECT(loan[sfPaymentRemaining] == 1);
3816 BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate));
3817 BEAST_EXPECT(loan[sfPrincipalOutstanding] ==
"1000000000");
3818 BEAST_EXPECT(loan[sfTotalValueOutstanding] ==
"1000000000");
3819 BEAST_EXPECT(!loan.isMember(sfLoanScale));
3821 loan[sfStartDate].asUInt() ==
3822 startDate.time_since_epoch().count());
3824 return loan[
"index"].asString();
3828 env.
close(startDate);
3831 env(
pay(lender, loanKeylet.key, broker.
asset(1000)));
3838 testcase <<
"Batch Bypass Counterparty";
3845 using namespace jtx;
3846 using namespace std::chrono_literals;
3849 Account const lender{
"lender"};
3850 Account const borrower{
"borrower"};
3859 createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3861 using namespace loan;
3863 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
3864 Number const principalRequest{1, 3};
3866 auto forgedLoanSet =
3867 set(borrower, broker.
brokerID, principalRequest, 0);
3872 sigObject[jss::SigningPubKey] =
strHex(lender.
pk().
slice());
3875 parse(randomData).addWithoutSigningFields(ss);
3884 auto const seq = env.
seq(borrower);
3897 params[jss::account] = borrower.
human();
3898 params[jss::type] =
"Loan";
3901 auto const objects = res[jss::result][jss::account_objects];
3902 BEAST_EXPECT(objects.size() == 0);
3910 testcase <<
"Wrong Max Debt Behavior";
3912 using namespace jtx;
3913 using namespace std::chrono_literals;
3916 Account const issuer{
"issuer"};
3917 Account const lender{
"lender"};
3921 XRP(brokerParams.vaultDeposit * 100), issuer,
noripple(lender));
3927 createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3930 BEAST_EXPECT(brokerSle))
3932 BEAST_EXPECT(brokerSle->at(sfDebtMaximum) == 0);
3935 using namespace loan;
3937 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
3938 Number const principalRequest{1, 3};
3940 auto createJson = env.
json(
3941 set(lender, broker.
brokerID, principalRequest),
fee(loanSetFee));
3944 counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature];
3945 counterpartyJson[sfSigningPubKey] = createJson[sfSigningPubKey];
3946 if (!BEAST_EXPECT(!createJson.isMember(jss::Signers)))
3947 counterpartyJson[sfSigners] = createJson[sfSigners];
3949 createJson = env.
json(
3950 createJson,
json(sfCounterpartySignature, counterpartyJson));
3960 testcase <<
"LoanPay xrpl::detail::computePeriodicPayment : "
3963 using namespace jtx;
3964 using namespace std::chrono_literals;
3967 Account const issuer{
"issuer"};
3968 Account const lender{
"lender"};
3969 Account const borrower{
"borrower"};
3978 createVaultAndBroker(env, xrpAsset, lender, brokerParams)};
3980 using namespace loan;
3982 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
3983 Number const principalRequest{640562, -5};
3985 Number const serviceFee{2462611968};
3988 auto createJson = env.
json(
3991 loanServiceFee(serviceFee),
3992 paymentTotal(numPayments),
3995 createJson[
"CloseInterestRate"] = 55374;
3996 createJson[
"ClosePaymentFee"] =
"3825205248";
3997 createJson[
"GracePeriod"] = 0;
3998 createJson[
"LatePaymentFee"] =
"237";
3999 createJson[
"LoanOriginationFee"] =
"0";
4000 createJson[
"OverpaymentFee"] = 35167;
4001 createJson[
"OverpaymentInterestRate"] = 1360;
4002 createJson[
"PaymentInterval"] = 727;
4004 auto const brokerStateBefore =
4006 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4009 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
4015 BEAST_EXPECT(!env.
le(keylet));
4017 Number const actualPrincipal{6};
4019 createJson[sfPrincipalRequested] = actualPrincipal;
4020 createJson.removeMember(sfSequence.jsonName);
4021 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
4035 using namespace jtx;
4039 auto lowerFee = [&]() {
4045 auto const baseFee = env.
current()->fees().base;
4049 std::string const borrowerSeed =
"ssBRAsLpH4778sLNYC4ik1JBJsBVf";
4051 auto const lenderPass =
"lender";
4052 std::string const lenderSeed =
"shPTCZGwTEhJrYT8NbcNkeaa8pzPM";
4055 env.
fund(
XRP(1'000'000), alice, lender, borrower);
4065 testcase(
"RPC AccountSet");
4067 txJson[sfTransactionType] =
"AccountSet";
4068 txJson[sfAccount] = borrower.
human();
4070 auto const signParams = [&]() {
4072 signParams[jss::passphrase] = borrowerPass;
4073 signParams[jss::key_type] =
"ed25519";
4074 signParams[jss::tx_json] = txJson;
4077 auto const jSign = env.
rpc(
"json",
"sign",
to_string(signParams));
4079 jSign.isMember(jss::result) &&
4080 jSign[jss::result].isMember(jss::tx_json));
4081 auto txSignResult = jSign[jss::result][jss::tx_json];
4082 auto txSignBlob = jSign[jss::result][jss::tx_blob].asString();
4083 txSignResult.removeMember(jss::hash);
4085 auto const jtx = env.
jt(txJson,
sig(borrower));
4086 BEAST_EXPECT(txSignResult == jtx.jv);
4089 auto const jSubmit = env.
rpc(
"submit", txSignBlob);
4091 jSubmit.isMember(jss::result) &&
4092 jSubmit[jss::result].isMember(jss::engine_result) &&
4093 jSubmit[jss::result][jss::engine_result].asString() ==
4101 testcase(
"RPC LoanSet - illegal signature_target");
4104 txJson[sfTransactionType] =
"AccountSet";
4105 txJson[sfAccount] = borrower.
human();
4107 auto const borrowerSignParams = [&]() {
4109 params[jss::passphrase] = borrowerPass;
4110 params[jss::key_type] =
"ed25519";
4111 params[jss::signature_target] =
"Destination";
4112 params[jss::tx_json] = txJson;
4115 auto const jSignBorrower =
4116 env.
rpc(
"json",
"sign",
to_string(borrowerSignParams));
4118 jSignBorrower.isMember(jss::result) &&
4119 jSignBorrower[jss::result].isMember(jss::error) &&
4120 jSignBorrower[jss::result][jss::error] ==
"invalidParams" &&
4121 jSignBorrower[jss::result].isMember(jss::error_message) &&
4122 jSignBorrower[jss::result][jss::error_message] ==
4126 testcase(
"RPC LoanSet - sign and submit borrower initiated");
4129 txJson[sfTransactionType] =
"LoanSet";
4130 txJson[sfAccount] = borrower.
human();
4131 txJson[sfCounterparty] = lender.human();
4132 txJson[sfLoanBrokerID] =
4133 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
4136 txJson[sfPrincipalRequested] =
"100000000";
4137 txJson[sfPaymentTotal] = 10000;
4138 txJson[sfPaymentInterval] = 3600;
4139 txJson[sfGracePeriod] = 300;
4140 txJson[sfFlags] = 65536;
4141 txJson[sfFee] =
to_string(24 * baseFee / 10);
4144 auto const borrowerSignParams = [&]() {
4146 params[jss::passphrase] = borrowerPass;
4147 params[jss::key_type] =
"ed25519";
4148 params[jss::tx_json] = txJson;
4151 auto const jSignBorrower =
4152 env.
rpc(
"json",
"sign",
to_string(borrowerSignParams));
4154 jSignBorrower.isMember(jss::result) &&
4155 jSignBorrower[jss::result].isMember(jss::tx_json),
4157 auto const txBorrowerSignResult =
4158 jSignBorrower[jss::result][jss::tx_json];
4159 auto const txBorrowerSignBlob =
4160 jSignBorrower[jss::result][jss::tx_blob].asString();
4166 auto const jSubmitBlob = env.
rpc(
"submit", txBorrowerSignBlob);
4167 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4168 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4169 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4173 jSubmitBlobResult.isMember(jss::engine_result) &&
4174 jSubmitBlobResult[jss::engine_result].asString() ==
4180 auto const lenderSignParams = [&]() {
4182 params[jss::passphrase] = lenderPass;
4183 params[jss::key_type] =
"ed25519";
4184 params[jss::signature_target] =
"CounterpartySignature";
4185 params[jss::tx_json] = txBorrowerSignResult;
4188 auto const jSignLender =
4191 jSignLender.isMember(jss::result) &&
4192 jSignLender[jss::result].isMember(jss::tx_json));
4193 auto const txLenderSignResult =
4194 jSignLender[jss::result][jss::tx_json];
4195 auto const txLenderSignBlob =
4196 jSignLender[jss::result][jss::tx_blob].asString();
4200 auto const jSubmitBlob = env.
rpc(
"submit", txLenderSignBlob);
4201 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4202 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4203 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4204 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
4209 jSubmitBlobResult.isMember(jss::engine_result) &&
4210 jSubmitBlobResult[jss::engine_result].asString() ==
4215 !jSubmitBlob.isMember(jss::error) &&
4216 !jSubmitBlobResult.isMember(jss::error));
4223 auto const jSubmitJson =
4224 env.
rpc(
"json",
"submit",
to_string(lenderSignParams));
4225 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
4226 auto const jSubmitJsonResult = jSubmitJson[jss::result];
4227 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
4228 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
4232 jSubmitJsonResult.isMember(jss::engine_result) &&
4233 jSubmitJsonResult[jss::engine_result].asString() ==
4238 !jSubmitJson.isMember(jss::error) &&
4239 !jSubmitJsonResult.isMember(jss::error));
4241 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
4245 testcase(
"RPC LoanSet - sign and submit lender initiated");
4248 txJson[sfTransactionType] =
"LoanSet";
4249 txJson[sfAccount] = lender.human();
4250 txJson[sfCounterparty] = borrower.
human();
4251 txJson[sfLoanBrokerID] =
4252 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
4255 txJson[sfPrincipalRequested] =
"100000000";
4256 txJson[sfPaymentTotal] = 10000;
4257 txJson[sfPaymentInterval] = 3600;
4258 txJson[sfGracePeriod] = 300;
4259 txJson[sfFlags] = 65536;
4260 txJson[sfFee] =
to_string(24 * baseFee / 10);
4263 auto const lenderSignParams = [&]() {
4265 params[jss::passphrase] = lenderPass;
4266 params[jss::key_type] =
"ed25519";
4267 params[jss::tx_json] = txJson;
4270 auto const jSignLender =
4273 jSignLender.isMember(jss::result) &&
4274 jSignLender[jss::result].isMember(jss::tx_json));
4275 auto const txLenderSignResult =
4276 jSignLender[jss::result][jss::tx_json];
4277 auto const txLenderSignBlob =
4278 jSignLender[jss::result][jss::tx_blob].asString();
4284 auto const jSubmitBlob = env.
rpc(
"submit", txLenderSignBlob);
4285 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4286 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4287 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4291 jSubmitBlobResult.isMember(jss::engine_result) &&
4292 jSubmitBlobResult[jss::engine_result].asString() ==
4298 auto const borrowerSignParams = [&]() {
4300 params[jss::passphrase] = borrowerPass;
4301 params[jss::key_type] =
"ed25519";
4302 params[jss::signature_target] =
"CounterpartySignature";
4303 params[jss::tx_json] = txLenderSignResult;
4306 auto const jSignBorrower =
4307 env.
rpc(
"json",
"sign",
to_string(borrowerSignParams));
4309 jSignBorrower.isMember(jss::result) &&
4310 jSignBorrower[jss::result].isMember(jss::tx_json));
4311 auto const txBorrowerSignResult =
4312 jSignBorrower[jss::result][jss::tx_json];
4313 auto const txBorrowerSignBlob =
4314 jSignBorrower[jss::result][jss::tx_blob].asString();
4318 auto const jSubmitBlob = env.
rpc(
"submit", txBorrowerSignBlob);
4319 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
4320 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
4321 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
4322 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
4327 jSubmitBlobResult.isMember(jss::engine_result) &&
4328 jSubmitBlobResult[jss::engine_result].asString() ==
4333 !jSubmitBlob.isMember(jss::error) &&
4334 !jSubmitBlobResult.isMember(jss::error));
4341 auto const jSubmitJson =
4342 env.
rpc(
"json",
"submit",
to_string(borrowerSignParams));
4343 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
4344 auto const jSubmitJsonResult = jSubmitJson[jss::result];
4345 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
4346 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
4350 jSubmitJsonResult.isMember(jss::engine_result) &&
4351 jSubmitJsonResult[jss::engine_result].asString() ==
4356 !jSubmitJson.isMember(jss::error) &&
4357 !jSubmitJsonResult.isMember(jss::error));
4359 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
4366 testcase <<
"Service Fee On Broker Deep Freeze";
4367 using namespace jtx;
4368 using namespace loan;
4369 Account const issuer(
"issuer");
4370 Account const borrower(
"borrower");
4371 Account const broker(
"broker");
4372 auto const IOU = issuer[
"IOU"];
4374 for (
bool const deepFreeze : {
true,
false})
4378 auto getCoverBalance = [&](
BrokerInfo const& brokerInfo,
4379 auto const& accountField) {
4384 auto const account = le->at(accountField);
4386 BEAST_EXPECT(sleLine))
4389 if (account > issuer.
id())
4397 env.
fund(
XRP(20'000), issuer, broker, borrower);
4400 env(
trust(broker,
IOU(20'000'000)));
4401 env(
pay(issuer, broker,
IOU(10'000'000)));
4404 auto const brokerInfo = createVaultAndBroker(env,
IOU, broker);
4406 BEAST_EXPECT(getCoverBalance(brokerInfo, sfAccount) ==
IOU(1'000));
4411 sig(sfCounterpartySignature, broker),
4412 loanServiceFee(
IOU(100).value()),
4413 paymentInterval(100),
4417 env(
trust(borrower,
IOU(20'000'000)));
4420 env(
pay(issuer, borrower,
IOU(500)));
4442 getCoverBalance(brokerInfo, sfAccount) ==
IOU(1'100));
4444 getCoverBalance(brokerInfo, sfOwner) ==
IOU(8'999'000));
4450 getCoverBalance(brokerInfo, sfOwner) ==
IOU(8'999'100));
4452 getCoverBalance(brokerInfo, sfAccount) ==
IOU(1'000));
4462 testcase(
"Basic Math");
4470 testcase <<
"Issuer Loan";
4472 using namespace jtx;
4473 using namespace loan;
4474 Account const issuer(
"issuer");
4475 Account const borrower = issuer;
4476 Account const lender(
"lender");
4479 env.
fund(
XRP(1'000), issuer, lender);
4485 .holders = {lender},
4486 .pay = issuerBalance});
4492 createVaultAndBroker(env, asset, lender, brokerParams);
4493 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
4496 sig(sfCounterpartySignature, lender),
4502 BEAST_EXPECT(env.
balance(issuer, asset) == asset(-issuerBalance + 200));
4505 env(
pay(borrower, loanKeylet.key, asset(200)));
4508 BEAST_EXPECT(env.
balance(issuer, asset) == asset(-issuerBalance));
4514 testcase(
"Invalid LoanDelete");
4515 using namespace jtx;
4516 using namespace loan;
4531 testcase(
"Invalid LoanManage");
4532 using namespace jtx;
4533 using namespace loan;
4548 testcase(
"Invalid LoanPay");
4549 using namespace jtx;
4550 using namespace loan;
4551 Account const lender{
"lender"};
4552 Account const issuer{
"issuer"};
4553 Account const borrower{
"borrower"};
4554 auto const IOU = issuer[
"IOU"];
4558 env.
fund(
XRP(1'000), lender, issuer, borrower);
4559 env(
trust(lender,
IOU(10'000'000)), THISLINE);
4560 env(
pay(issuer, lender,
IOU(5'000'000)), THISLINE);
4561 BrokerInfo brokerInfo{createVaultAndBroker(env, issuer[
"IOU"], lender)};
4563 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
4566 env(
set(borrower, brokerInfo.brokerID, debtMaximumRequest),
4567 sig(sfCounterpartySignature, lender),
4574 auto const loanKeylet =
keylet::loan(brokerInfo.brokerID, loanSequence);
4580 env(
pay(borrower, loanKeylet.key, debtMaximumRequest),
4589 if (
auto brokerSle =
4591 BEAST_EXPECT(brokerSle))
4593 return Account{
"pseudo", brokerSle->at(sfAccount)};
4606 lender[
"IOU"](1'000),
4612 (*pseudoBroker)[
"IOU"](1'000),
4619 env(
pay(borrower, loanKeylet.key, debtMaximumRequest),
4627 lender[
"IOU"](1'000),
4633 env(
pay(borrower, loanKeylet.key, debtMaximumRequest),
4647 env(
pay(borrower, loanKeylet.key, debtMaximumRequest),
4655 testcase(
"Invalid LoanSet");
4656 using namespace jtx;
4657 using namespace loan;
4658 Account const lender{
"lender"};
4659 Account const issuer{
"issuer"};
4660 Account const borrower{
"borrower"};
4661 auto const IOU = issuer[
"IOU"];
4663 auto testWrapper = [&](
auto&& test) {
4665 env.
fund(
XRP(1'000), lender, issuer, borrower);
4666 env(
trust(lender,
IOU(10'000'000)));
4667 env(
pay(issuer, lender,
IOU(5'000'000)));
4669 createVaultAndBroker(env, issuer[
"IOU"], lender)};
4671 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
4672 Number const debtMaximumRequest = brokerInfo.asset(1'000).value();
4673 test(env, brokerInfo, loanSetFee, debtMaximumRequest);
4677 testWrapper([&](
Env& env,
4680 Number const& debtMaximumRequest) {
4685 auto jv =
set(borrower,
uint256{}, debtMaximumRequest);
4687 auto testZeroBrokerID = [&](
std::string const& id,
4690 jv[sfLoanBrokerID] = id;
4692 sig(sfCounterpartySignature, lender),
4710 set(borrower, brokerInfo.
brokerID, debtMaximumRequest),
4711 sig(sfCounterpartySignature, lender),
4714 auto counterpartySig =
4716 auto badPubKey = counterpartySig.
getFieldVL(sfSigningPubKey);
4717 badPubKey[20] ^= 0xAA;
4718 counterpartySig.setFieldVL(sfSigningPubKey, badPubKey);
4722 auto res = env.
rpc(
"json",
"submit",
to_string(jvResult))[
"result"];
4724 res[jss::error] ==
"invalidTransaction" &&
4725 res[jss::error_exception] ==
4726 "fails local checks: Counterparty: Invalid signature.");
4730 testWrapper([&](
Env& env,
4733 Number const& debtMaximumRequest) {
4739 env(
set(borrower, brokerInfo.
brokerID, debtMaximumRequest),
4740 sig(sfCounterpartySignature, lender),
4746 testWrapper([&](
Env& env,
4749 Number const& debtMaximumRequest) {
4750 auto const amt = env.
balance(borrower) -
4752 env(
pay(borrower, issuer, amt));
4755 env(
set(borrower, brokerInfo.
brokerID, debtMaximumRequest),
4756 sig(sfCounterpartySignature, lender),
4761 env(
pay(issuer, borrower, amt));
4765 env(
set(borrower, brokerInfo.
brokerID, debtMaximumRequest),
4766 sig(sfCounterpartySignature, lender),
4776 testcase <<
"LoanSet trigger xrpl::accountSendMPT : minimum amount "
4779 using namespace jtx;
4780 using namespace std::chrono_literals;
4783 Account const issuer{
"issuer"};
4784 Account const lender{
"lender"};
4785 Account const borrower{
"borrower"};
4787 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
4794 mptt.authorize({.account = lender});
4795 mptt.authorize({.account = borrower});
4796 env(
pay(issuer, lender, mptAsset(2'000'000)));
4797 env(
pay(issuer, borrower, mptAsset(1'000)));
4800 BrokerInfo broker{createVaultAndBroker(env, mptAsset, lender)};
4802 using namespace loan;
4804 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
4805 Number const principalRequest{1, 3};
4807 auto createJson = env.
json(
4812 createJson[
"CloseInterestRate"] = 76671;
4813 createJson[
"ClosePaymentFee"] =
"2061925410";
4814 createJson[
"GracePeriod"] = 434;
4815 createJson[
"InterestRate"] = 50302;
4816 createJson[
"LateInterestRate"] = 30322;
4817 createJson[
"LatePaymentFee"] =
"294427911";
4818 createJson[
"LoanOriginationFee"] =
"3250635102";
4819 createJson[
"LoanServiceFee"] =
"9557386";
4820 createJson[
"OverpaymentFee"] = 51249;
4821 createJson[
"OverpaymentInterestRate"] = 14304;
4822 createJson[
"PaymentInterval"] = 434;
4823 createJson[
"PaymentTotal"] =
"2891743748";
4824 createJson[
"PrincipalRequested"] =
"8516.98";
4826 auto const brokerStateBefore =
4829 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
4838 testcase <<
"LoanPay xrpl::LoanPay::doApply : debtDecrease "
4841 using namespace jtx;
4842 using namespace std::chrono_literals;
4843 using namespace Lending;
4846 Account const issuer{
"issuer"};
4847 Account const lender{
"lender"};
4848 Account const borrower{
"borrower"};
4850 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
4854 auto trustLenderTx = env.
json(
trust(lender, iouAsset(1'000'000'000)));
4856 auto trustBorrowerTx =
4857 env.
json(
trust(borrower, iouAsset(1'000'000'000)));
4858 env(trustBorrowerTx);
4859 auto payLenderTx =
pay(issuer, lender, iouAsset(100'000'000));
4861 auto payIssuerTx =
pay(issuer, borrower, iouAsset(1'000'000));
4865 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
4867 using namespace loan;
4869 auto const baseFee = env.
current()->fees().base;
4870 auto const loanSetFee =
fee(baseFee * 2);
4871 Number const principalRequest{1, 3};
4873 auto createJson = env.
json(
4878 createJson[
"ClosePaymentFee"] =
"0";
4879 createJson[
"GracePeriod"] = 60;
4880 createJson[
"InterestRate"] = 24346;
4881 createJson[
"LateInterestRate"] = 65535;
4882 createJson[
"LatePaymentFee"] =
"0";
4883 createJson[
"LoanOriginationFee"] =
"218";
4884 createJson[
"LoanServiceFee"] =
"0";
4885 createJson[
"PaymentInterval"] = 60;
4886 createJson[
"PaymentTotal"] = 5678;
4887 createJson[
"PrincipalRequested"] =
"9924.81";
4889 auto const brokerStateBefore =
4891 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4894 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
4898 auto const pseudoAcct = [&]() {
4900 if (!BEAST_EXPECT(brokerSle))
4902 auto const brokerPseudo = brokerSle->at(sfAccount);
4903 return Account(
"Broker pseudo-account", brokerPseudo);
4907 auto const originalState = getCurrentState(env, broker, keylet);
4908 verifyLoanStatus(originalState);
4910 Number const payment{3'269'349'176'470'588, -12};
4913 ((payment / originalState.periodicPayment) /
4914 loanPaymentsPerFeeIncrement +
4916 auto loanPayTx = env.
json(
4919 BEAST_EXPECT(
to_string(payment) ==
"3269.349176470588");
4923 auto const newState = getCurrentState(env, broker, keylet);
4926 newState.managementFeeOutstanding,
4927 originalState.loanScale));
4929 newState.managementFeeOutstanding <
4930 originalState.managementFeeOutstanding);
4932 broker.
asset, newState.totalValue, originalState.loanScale));
4935 newState.principalOutstanding,
4936 originalState.loanScale));
4943 testcase <<
"xrpl::loanComputePaymentParts : valid total interest";
4945 using namespace jtx;
4946 using namespace std::chrono_literals;
4949 Account const issuer{
"issuer"};
4950 Account const lender{
"lender"};
4951 Account const borrower{
"borrower"};
4953 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
4957 auto trustLenderTx = env.
json(trust(lender, iouAsset(1'000'000'000)));
4959 auto trustBorrowerTx =
4960 env.
json(trust(borrower, iouAsset(1'000'000'000)));
4961 env(trustBorrowerTx);
4962 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4964 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
4968 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
4970 using namespace loan;
4972 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
4973 Number const principalRequest{1, 3};
4974 auto const startDate = env.
now() + 60s;
4976 auto createJson = env.
json(
4981 createJson[
"CloseInterestRate"] = 47299;
4982 createJson[
"ClosePaymentFee"] =
"3985819770";
4983 createJson[
"GracePeriod"] = 0;
4984 createJson[
"InterestRate"] = 92;
4985 createJson[
"LatePaymentFee"] =
"3866894865";
4986 createJson[
"LoanOriginationFee"] =
"0";
4987 createJson[
"LoanServiceFee"] =
"2348810240";
4988 createJson[
"OverpaymentFee"] = 58545;
4989 createJson[
"PaymentInterval"] = 60;
4990 createJson[
"PaymentTotal"] = 1;
4991 createJson[
"PrincipalRequested"] =
"0.000763058";
4993 auto const brokerStateBefore =
4995 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4998 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
5000 env.
close(startDate);
5002 auto loanPayTx = env.
json(
5003 pay(borrower, keylet.
key,
STAmount{broker.asset, Number{}}));
5004 loanPayTx[
"Amount"][
"value"] =
"0.000281284125490196";
5013 testcase <<
"DoS LoanPay";
5015 using namespace jtx;
5016 using namespace std::chrono_literals;
5017 using namespace Lending;
5018 Env env(*
this, all);
5020 Account const issuer{
"issuer"};
5021 Account const lender{
"lender"};
5022 Account const borrower{
"borrower"};
5024 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
5028 env(trust(lender, iouAsset(100'000'000)));
5029 env(trust(borrower, iouAsset(100'000'000)));
5030 env(pay(issuer, lender, iouAsset(10'000'000)));
5031 env(pay(issuer, borrower, iouAsset(1'000)));
5034 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
5036 using namespace loan;
5038 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
5039 Number const principalRequest{1, 3};
5040 auto const baseFee = env.
current()->fees().base;
5042 auto createJson = env.
json(
5047 createJson[
"ClosePaymentFee"] =
"0";
5048 createJson[
"GracePeriod"] = 60;
5049 createJson[
"InterestRate"] = 20930;
5050 createJson[
"LateInterestRate"] = 77049;
5051 createJson[
"LatePaymentFee"] =
"0";
5052 createJson[
"LoanServiceFee"] =
"0";
5053 createJson[
"OverpaymentFee"] = 7;
5054 createJson[
"OverpaymentInterestRate"] = 66653;
5055 createJson[
"PaymentInterval"] = 60;
5056 createJson[
"PaymentTotal"] = 3239184;
5057 createJson[
"PrincipalRequested"] =
"3959.37";
5059 auto const brokerStateBefore =
5061 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5062 auto const keylet = keylet::loan(broker.
brokerID, loanSequence);
5064 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
5068 auto const stateBefore = getCurrentState(env, broker, keylet);
5069 BEAST_EXPECT(stateBefore.paymentRemaining == 3239184);
5071 stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction);
5073 auto loanPayTx = env.
json(
5074 pay(borrower, keylet.
key,
STAmount{broker.asset, Number{}}));
5076 loanPayTx[
"Amount"][
"value"] = to_string(amount);
5080 amount / stateBefore.periodicPayment /
5081 loanPaymentsPerFeeIncrement +
5083 env(loanPayTx, ter(tesSUCCESS), fee(payFee));
5086 auto const stateAfter = getCurrentState(env, broker, keylet);
5088 stateAfter.paymentRemaining ==
5089 stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction);
5096 testcase <<
"xrpl::loanComputePaymentParts : totalPrincipalPaid "
5099 using namespace jtx;
5100 using namespace std::chrono_literals;
5101 using namespace Lending;
5102 Env env(*
this, all);
5104 Account const issuer{
"issuer"};
5105 Account const lender{
"lender"};
5106 Account const borrower{
"borrower"};
5108 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
5112 auto trustLenderTx = env.
json(trust(lender, iouAsset(1'000'000'000)));
5114 auto trustBorrowerTx =
5115 env.
json(trust(borrower, iouAsset(1'000'000'000)));
5116 env(trustBorrowerTx);
5117 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
5119 auto payIssuerTx = pay(issuer, borrower, iouAsset(1'000'000));
5123 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
5125 using namespace loan;
5127 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
5128 Number const principalRequest{1, 3};
5130 auto createJson = env.
json(
5135 createJson[
"ClosePaymentFee"] =
"0";
5136 createJson[
"GracePeriod"] = 0;
5137 createJson[
"InterestRate"] = 24346;
5138 createJson[
"LateInterestRate"] = 65535;
5139 createJson[
"LatePaymentFee"] =
"0";
5140 createJson[
"LoanOriginationFee"] =
"218";
5141 createJson[
"LoanServiceFee"] =
"0";
5142 createJson[
"PaymentInterval"] = 60;
5143 createJson[
"PaymentTotal"] = 5678;
5144 createJson[
"PrincipalRequested"] =
"9924.81";
5146 auto const brokerStateBefore =
5148 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5149 auto const keylet = keylet::loan(broker.
brokerID, loanSequence);
5151 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
5155 auto const baseFee = env.
current()->fees().base;
5157 auto const stateBefore = getCurrentState(env, broker, keylet);
5160 auto loanPayTx = env.
json(
5161 pay(borrower, keylet.
key,
STAmount{broker.asset, Number{}}));
5163 BEAST_EXPECT(to_string(
amount) ==
"3074.745058823529");
5166 (
amount / stateBefore.periodicPayment /
5167 loanPaymentsPerFeeIncrement +
5169 loanPayTx[
"Amount"][
"value"] = to_string(
amount);
5175 auto loanPayTx = env.
json(
5176 pay(borrower, keylet.
key,
STAmount{broker.asset, Number{}}));
5177 Number const amount{6732'118'170'944'051, -12};
5178 BEAST_EXPECT(to_string(amount) ==
"6732.118170944051");
5179 XRPAmount
const payFee{
5181 (amount / stateBefore.periodicPayment /
5182 loanPaymentsPerFeeIncrement +
5184 loanPayTx[
"Amount"][
"value"] =
to_string(amount);
5185 env(loanPayTx, fee(payFee), ter(tesSUCCESS));
5189 auto const stateAfter = getCurrentState(env, broker, keylet);
5191 BEAST_EXPECT(stateAfter.totalValue >= stateAfter.principalOutstanding);
5194 stateBefore.principalOutstanding >=
5195 stateAfter.principalOutstanding);
5197 BEAST_EXPECT(stateBefore.totalValue >= stateAfter.totalValue);
5201 (stateBefore.totalValue - stateAfter.totalValue) >=
5202 (stateBefore.principalOutstanding -
5203 stateAfter.principalOutstanding));
5210 testcase <<
"xrpl::loanComputePaymentParts : loanValueChange rounded";
5212 using namespace jtx;
5213 using namespace std::chrono_literals;
5214 using namespace Lending;
5215 Env env(*
this, all);
5217 Account const issuer{
"issuer"};
5218 Account const lender{
"lender"};
5219 Account const borrower{
"borrower"};
5221 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
5225 auto trustLenderTx = env.
json(trust(lender, iouAsset(1'000'000'000)));
5227 auto trustBorrowerTx =
5228 env.
json(trust(borrower, iouAsset(1'000'000'000)));
5229 env(trustBorrowerTx);
5230 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
5232 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
5236 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender)};
5238 auto const coverDepositValue =
5240 env(loanBroker::coverDeposit(
5241 lender, broker.
brokerID, coverDepositValue));
5245 using namespace loan;
5247 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
5248 Number const principalRequest{1, 3};
5250 auto createJson = env.
json(
5255 createJson[
"ClosePaymentFee"] =
"0";
5256 createJson[
"GracePeriod"] = 0;
5257 createJson[
"InterestRate"] = 12833;
5258 createJson[
"LateInterestRate"] = 77048;
5259 createJson[
"LatePaymentFee"] =
"0";
5260 createJson[
"LoanOriginationFee"] =
"218";
5261 createJson[
"LoanServiceFee"] =
"0";
5262 createJson[
"PaymentInterval"] = 752;
5263 createJson[
"PaymentTotal"] = 5678;
5264 createJson[
"PrincipalRequested"] =
"9924.81";
5266 auto const brokerStateBefore =
5268 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5269 auto const keylet = keylet::loan(broker.
brokerID, loanSequence);
5271 createJson = env.
json(createJson,
sig(sfCounterpartySignature, lender));
5275 auto const baseFee = env.
current()->fees().base;
5277 auto const stateBefore = getCurrentState(env, broker, keylet);
5278 BEAST_EXPECT(stateBefore.paymentRemaining == 5678);
5280 stateBefore.paymentRemaining > loanMaximumPaymentsPerTransaction);
5282 auto loanPayTx = env.
json(
5283 pay(borrower, keylet.
key,
STAmount{broker.asset, Number{}}));
5285 BEAST_EXPECT(to_string(amount) ==
"9924.81");
5288 (amount / stateBefore.periodicPayment /
5289 loanPaymentsPerFeeIncrement +
5291 loanPayTx[
"Amount"][
"value"] = to_string(amount);
5292 env(loanPayTx, fee(payFee), ter(tesSUCCESS));
5295 auto const stateAfter = getCurrentState(env, broker, keylet);
5297 stateAfter.paymentRemaining ==
5298 stateBefore.paymentRemaining - loanMaximumPaymentsPerTransaction);
5305 testcase <<
"Prevent nextPaymentDueDate overflow";
5307 using namespace jtx;
5308 using namespace std::chrono_literals;
5309 using namespace Lending;
5310 Env env(*
this, all);
5312 Account const issuer{
"issuer"};
5313 Account const lender{
"lender"};
5314 Account const borrower{
"borrower"};
5316 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
5320 auto trustLenderTx = env.
json(trust(lender, iouAsset(1'000'000'000)));
5322 auto trustBorrowerTx =
5323 env.
json(trust(borrower, iouAsset(1'000'000'000)));
5324 env(trustBorrowerTx);
5325 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
5327 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
5334 createVaultAndBroker(env, iouAsset, lender, brokerParams)};
5336 using namespace loan;
5338 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
5340 using timeType =
decltype(sfNextPaymentDueDate)::type::value_type;
5343 static_assert(maxTime == 4'294'967'295);
5345 auto const baseJson = [&]() {
5346 auto createJson = env.
json(
5354 loanOriginationFee(218),
5362 auto const baseFee = env.
current()->fees().base;
5364 auto parentCloseTime = [&]() {
5365 return env.
current()->parentCloseTime().time_since_epoch().count();
5367 auto maxLoanTime = [&]() {
5368 auto const startDate = parentCloseTime();
5370 BEAST_EXPECT(startDate >= 50);
5372 return maxTime - startDate;
5377 auto const interval = maxLoanTime() + 1;
5378 auto const total = 1;
5379 auto createJson = env.
json(
5380 baseJson, paymentInterval(interval), paymentTotal(total));
5383 sig(sfCounterpartySignature, lender),
5390 auto const interval = 60;
5391 auto const total = maxLoanTime() + 1;
5392 auto createJson = env.
json(
5396 sig(sfCounterpartySignature, lender),
5403 auto const interval = maxLoanTime() + 1;
5404 auto const total = 1;
5405 auto const grace = interval;
5406 auto createJson = env.
json(
5414 sig(sfCounterpartySignature, lender),
5420 auto const interval = 1'000'000'000;
5421 auto const total = 10;
5422 auto createJson = env.
json(
5426 sig(sfCounterpartySignature, lender),
5433 auto const interval = 60;
5434 auto const total = 1'000'000'000;
5435 auto createJson = env.
json(
5439 sig(sfCounterpartySignature, lender),
5446 auto const total = 60;
5447 auto const interval = (maxLoanTime() - total) / total;
5448 auto const grace = interval;
5449 auto createJson = env.
json(
5456 sig(sfCounterpartySignature, lender),
5462 auto const brokerStateBefore =
5464 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5467 auto const grace = 100;
5468 auto const interval = maxLoanTime() - grace;
5469 auto const total = 1;
5470 auto createJson = env.
json(
5477 sig(sfCounterpartySignature, lender),
5482 auto const meta = env.
meta();
5483 if (BEAST_EXPECT(meta))
5485 BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED);
5489 auto const loanSle = env.
le(keylet);
5491 BEAST_EXPECT(!loanSle);
5495 auto const brokerStateBefore =
5497 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
5500 auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10;
5501 auto const grace = 5'000;
5502 auto const interval = maxTime - closeStartDate - grace;
5503 auto const total = 1;
5504 auto createJson = env.
json(
5511 sig(sfCounterpartySignature, lender),
5516 auto const meta = env.
meta();
5517 if (BEAST_EXPECT(meta))
5519 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
5523 auto const afterState = getCurrentState(env, broker, keylet);
5524 BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace);
5525 BEAST_EXPECT(afterState.previousPaymentDate == 0);
5526 BEAST_EXPECT(afterState.paymentRemaining == 1);
5531 env(
pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2})));
5534 auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10;
5535 auto const grace = 5'000;
5536 auto const maxLoanTime = maxTime - closeStartDate - grace;
5537 auto const total = [&]() {
5538 if (maxLoanTime % 5 == 0)
5540 if (maxLoanTime % 3 == 0)
5542 if (maxLoanTime % 2 == 0)
5546 if (!BEAST_EXPECT(total != 0))
5549 auto const brokerState =
5552 auto const loanSequence = brokerState->at(sfLoanSequence);
5555 auto const interval = maxLoanTime / total;
5556 auto createJson = env.
json(
5563 sig(sfCounterpartySignature, lender),
5568 auto const beforeState = getCurrentState(env, broker, keylet);
5570 beforeState.nextPaymentDate == closeStartDate + interval);
5571 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
5572 BEAST_EXPECT(beforeState.paymentRemaining == total);
5573 BEAST_EXPECT(beforeState.periodicPayment > 0);
5576 Number
const payment = beforeState.periodicPayment * (total - 1);
5577 XRPAmount
const payFee{
5578 baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)};
5579 auto loanPayTx = env.
json(
5582 env(loanPayTx, ter(tesSUCCESS));
5586 auto const afterState = getCurrentState(env, broker, keylet);
5587 BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace);
5589 afterState.previousPaymentDate == maxTime - grace - interval);
5590 BEAST_EXPECT(afterState.paymentRemaining == 1);
5597 testcase(
"Require Auth - Implicit Pseudo-account authorization");
5598 using namespace jtx;
5599 using namespace loan;
5600 Account const lender{
"lender"};
5601 Account const issuer{
"issuer"};
5602 Account const borrower{
"borrower"};
5605 env.
fund(
XRP(100'000), issuer, lender, borrower);
5611 .holders = {lender, borrower},
5617 env(pay(issuer, lender, asset(5'000'000)));
5618 BrokerInfo brokerInfo{createVaultAndBroker(env, asset, lender)};
5620 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
5623 auto forUnauthAuth = [&](
auto&& doTx) {
5627 {.account = issuer, .holder = borrower, .flags = flag});
5635 forUnauthAuth([&](
bool authorized) {
5637 env(
set(borrower, brokerInfo.brokerID, debtMaximumRequest),
5638 sig(sfCounterpartySignature, lender),
5644 auto const loanKeylet = keylet::loan(brokerInfo.brokerID, loanSequence);
5647 forUnauthAuth([&](
bool authorized) {
5649 env(pay(borrower, loanKeylet.key, debtMaximumRequest), err);
5657 "CoverDeposit and CoverWithdraw reject MPT without CanTransfer");
5658 using namespace jtx;
5659 using namespace loanBroker;
5661 Env env(*
this, all);
5663 Account const issuer{
"issuer"};
5666 env.
fund(
XRP(100'000), issuer, alice);
5669 MPTTester mpt{env, issuer, mptInitNoFund};
5678 mpt.authorize({.account = alice});
5682 env(pay(issuer, alice, asset(100)));
5686 auto const [
createTx, vaultKeylet] =
5687 vault.create({.owner = alice, .asset = asset});
5691 auto const brokerKeylet =
5692 keylet::loanbroker(alice.id(), env.
seq(alice));
5693 env(
set(alice, vaultKeylet.key));
5696 auto const brokerSle = env.
le(brokerKeylet);
5697 if (!BEAST_EXPECT(brokerSle))
5701 "Loan Broker pseudo-account", brokerSle->at(sfAccount)};
5712 auto const depositAmount = asset(1);
5713 env(coverDeposit(alice, brokerKeylet.key, depositAmount),
5717 if (
auto const refreshed = env.
le(brokerKeylet);
5718 BEAST_EXPECT(refreshed))
5720 BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0);
5728 env(coverDeposit(alice, brokerKeylet.key, depositAmount));
5731 if (
auto const refreshed = env.
le(brokerKeylet);
5732 BEAST_EXPECT(refreshed))
5734 BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 1);
5743 env(coverWithdraw(alice, brokerKeylet.key, depositAmount),
5751 env(coverWithdraw(alice, brokerKeylet.key, depositAmount));
5754 if (
auto const refreshed = env.
le(brokerKeylet);
5755 BEAST_EXPECT(refreshed))
5757 BEAST_EXPECT(refreshed->at(sfCoverAvailable) == 0);
5764 testLoanPayLateFullPaymentBypassesPenalties()
5766 testcase(
"LoanPay full payment skips late penalties");
5767 using namespace jtx;
5768 using namespace loan;
5769 using namespace std::chrono_literals;
5771 Env env(*
this, all);
5773 Account const issuer{
"issuer"};
5774 Account
const lender{
"lender"};
5775 Account
const borrower{
"borrower"};
5777 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
5780 PrettyAsset
const asset = issuer[iouCurrency];
5781 env(
trust(lender, asset(100'000'000)));
5782 env(
trust(borrower, asset(100'000'000)));
5783 env(
pay(issuer, lender, asset(50'000'000)));
5784 env(
pay(issuer, borrower, asset(5'000'000)));
5787 BrokerInfo broker{createVaultAndBroker(env, asset, lender)};
5789 auto const loanSetFee = fee(env.
current()->fees().base * 2);
5791 auto const brokerPreLoan = env.
le(keylet::loanbroker(broker.
brokerID));
5792 if (!BEAST_EXPECT(brokerPreLoan))
5795 auto const loanSequence = brokerPreLoan->at(sfLoanSequence);
5796 auto const loanKeylet = keylet::loan(broker.
brokerID, loanSequence);
5798 Number
const principal = asset(1'000).value();
5799 Number
const serviceFee = asset(2).value();
5800 Number
const lateFee = asset(5).value();
5801 Number
const closeFee = asset(4).value();
5804 sig(sfCounterpartySignature, lender),
5817 auto state1 = getCurrentState(env, broker, loanKeylet);
5818 if (!BEAST_EXPECT(state1.paymentRemaining > 1))
5821 using d = NetClock::duration;
5822 using tp = NetClock::time_point;
5823 auto const overdueClose =
5824 tp{d{state1.nextPaymentDate + state1.paymentInterval}};
5825 env.
close(overdueClose);
5827 auto const brokerSle = env.
le(keylet::loanbroker(broker.
brokerID));
5828 auto const loanSle = env.
le(loanKeylet);
5829 if (!BEAST_EXPECT(brokerSle && loanSle))
5832 auto state = getCurrentState(env, broker, loanKeylet);
5835 TenthBips32 const interestRateValue{loanSle->at(sfInterestRate)};
5837 loanSle->at(sfLateInterestRate)};
5839 loanSle->at(sfCloseInterestRate)};
5842 broker.
asset, loanSle->at(sfClosePaymentFee), state.loanScale);
5844 broker.
asset, loanSle->at(sfLatePaymentFee), state.loanScale);
5848 state.principalOutstanding,
5849 state.managementFeeOutstanding);
5850 Number
const totalInterestOutstanding = roundedLoanState.interestDue;
5852 auto const periodicRate =
5855 state.periodicPayment,
5857 state.paymentRemaining,
5860 auto const parentCloseTime = env.
current()->parentCloseTime();
5862 state.startDate.time_since_epoch().count());
5865 rawLoanState.principalOutstanding,
5868 state.paymentInterval,
5869 state.previousPaymentDate,
5871 closeInterestRateValue);
5873 Number
const roundedFullInterestAmount =
5877 roundedFullInterestAmount,
5880 Number
const roundedFullInterest =
5881 roundedFullInterestAmount - roundedFullManagementFee;
5883 Number
const trackedValueDelta = state.principalOutstanding +
5884 totalInterestOutstanding + state.managementFeeOutstanding;
5885 Number
const untrackedManagementFee = closePaymentFeeRounded +
5886 roundedFullManagementFee - state.managementFeeOutstanding;
5887 Number
const untrackedInterest =
5888 roundedFullInterest - totalInterestOutstanding;
5890 Number
const baseFullDue =
5891 trackedValueDelta + untrackedInterest + untrackedManagementFee;
5896 auto const overdueSeconds =
5897 parentCloseTime.time_since_epoch().count() - state.nextPaymentDate;
5898 if (!BEAST_EXPECT(overdueSeconds > 0))
5901 Number
const overdueRate =
5903 Number
const lateInterestRaw = state.principalOutstanding * overdueRate;
5904 Number
const lateInterestRounded =
5908 lateInterestRounded,
5911 Number
const penaltyDue = lateInterestRounded +
5912 lateManagementFeeRounded + latePaymentFeeRounded;
5913 BEAST_EXPECT(penaltyDue > Number{});
5917 STAmount
const paymentAmount{broker.
asset.
raw(), baseFullDue};
5918 env(
pay(borrower, loanKeylet.key, paymentAmount, tfLoanFullPayment));
5921 if (
auto const meta = env.
meta(); BEAST_EXPECT(meta))
5922 BEAST_EXPECT(meta->at(sfTransactionResult) ==
tesSUCCESS);
5925 Number
const actualPaid = balanceBefore - balanceAfter;
5926 BEAST_EXPECT(actualPaid == baseFullDue);
5928 Number
const expectedWithPenalty = baseFullDue + penaltyDue;
5929 BEAST_EXPECT(expectedWithPenalty > actualPaid);
5930 BEAST_EXPECT(expectedWithPenalty - actualPaid == penaltyDue);
5934 testLoanCoverMinimumRoundingExploit()
5936 auto testLoanCoverMinimumRoundingExploit =
5937 [&,
this](Number
const& principalRequest) {
5938 testcase <<
"LoanBrokerCoverClawback drains cover via rounding"
5939 <<
" principalRequested="
5942 using namespace jtx;
5943 using namespace loan;
5944 using namespace loanBroker;
5946 Env env(*
this, all);
5948 Account
const issuer{
"issuer"};
5949 Account
const lender{
"lender"};
5950 Account
const borrower{
"borrower"};
5952 env.
fund(
XRP(1'000'000'000), issuer, lender, borrower);
5955 env(
fset(issuer, asfAllowTrustLineClawback));
5958 PrettyAsset
const asset = issuer[iouCurrency];
5959 env(
trust(lender, asset(2'000'0000)));
5960 env(
trust(borrower, asset(2'000'0000)));
5963 env(
pay(issuer, lender, asset(2'000'0000)));
5966 BrokerParameters brokerParams{
5967 .debtMax = 0, .coverRateMin =
TenthBips32{10'000}};
5969 createVaultAndBroker(env, asset, lender, brokerParams)};
5971 auto const loanSetFee = fee(env.
current()->fees().base * 2);
5974 sig(sfCounterpartySignature, lender),
5982 auto const brokerBefore =
5984 BEAST_EXPECT(brokerBefore);
5988 Number
const debtOutstanding = brokerBefore->at(sfDebtTotal);
5989 Number
const coverAvailableBefore =
5990 brokerBefore->at(sfCoverAvailable);
5992 BEAST_EXPECT(debtOutstanding > Number{});
5993 BEAST_EXPECT(coverAvailableBefore > Number{});
5996 <<
" cover_available=" <<
to_string(coverAvailableBefore);
6001 auto const brokerAfter =
6003 BEAST_EXPECT(brokerAfter);
6007 Number
const debtAfter = brokerAfter->at(sfDebtTotal);
6009 BEAST_EXPECT(debtAfter == debtOutstanding);
6011 Number
const coverAvailableAfter =
6012 brokerAfter->at(sfCoverAvailable);
6016 BEAST_EXPECT(coverAvailableAfter != Number{});
6020 testLoanCoverMinimumRoundingExploit(Number{1, -30});
6021 testLoanCoverMinimumRoundingExploit(Number{1, -20});
6022 testLoanCoverMinimumRoundingExploit(Number{1, -10});
6023 testLoanCoverMinimumRoundingExploit(Number{1, 1});
6045 "PoC: Unsigned-underflow full-pay accrual after early periodic");
6047 using namespace jtx;
6048 using namespace loan;
6049 using namespace std::chrono_literals;
6051 Env env(*
this, all);
6053 Account const lender{
"poc_lender4"};
6054 Account const borrower{
"poc_borrower4"};
6055 env.
fund(
XRP(3'000'000), lender, borrower);
6061 createVaultAndBroker(env, asset, lender, brokerParams);
6065 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
6066 Number const principalRequest = asset(1000).value();
6067 auto const originationFee = asset(0).value();
6068 auto const serviceFee = asset(1).value();
6069 auto const serviceFeePA = asset(1);
6070 auto const lateFee = asset(0).value();
6071 auto const closeFee = asset(0).value();
6076 auto const total = 3u;
6077 auto const interval = 600u;
6078 auto const grace = 60u;
6080 auto createJtx = env.
jt(
6081 set(borrower, broker.
brokerID, principalRequest, 0),
6082 sig(sfCounterpartySignature, lender),
6083 loanOriginationFee(originationFee),
6084 loanServiceFee(serviceFee),
6085 latePaymentFee(lateFee),
6086 closePaymentFee(closeFee),
6088 interestRate(interest),
6089 lateInterestRate(lateInterest),
6090 closeInterestRate(closeInterest),
6091 overpaymentInterestRate(overpaymentInterest),
6092 paymentTotal(total),
6093 paymentInterval(interval),
6097 auto const brokerSle = env.
le(keylet::loanbroker(broker.
brokerID));
6098 BEAST_EXPECT(brokerSle);
6099 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
6100 auto const loanKeylet = keylet::loan(broker.
brokerID, loanSequence);
6106 auto state = getCurrentState(env, broker, loanKeylet);
6107 Number const periodicRate =
6109 auto const components = detail::computePaymentComponents(
6113 state.principalOutstanding,
6114 state.managementFeeOutstanding,
6115 state.periodicPayment,
6117 state.paymentRemaining,
6118 brokerParams.managementFeeRate);
6120 asset, components.trackedValueDelta + serviceFeePA.number()};
6122 env(pay(borrower, loanKeylet.key, regularDue));
6127 auto after = getCurrentState(env, broker, loanKeylet);
6128 auto const loanSle = env.
le(loanKeylet);
6129 BEAST_EXPECT(loanSle);
6130 auto const brokerSle2 = env.
le(keylet::loanbroker(broker.
brokerID));
6131 BEAST_EXPECT(brokerSle2);
6133 auto const closePaymentFee =
6134 loanSle ? loanSle->at(sfClosePaymentFee) :
Number{};
6135 auto const closeInterestRate = loanSle
6138 auto const managementFeeRate = brokerSle2
6139 ?
TenthBips16{brokerSle2->at(sfManagementFeeRate)}
6142 Number const periodicRate2 =
6147 after.periodicPayment,
6149 after.paymentRemaining,
6150 env.
current()->parentCloseTime(),
6151 after.paymentInterval,
6152 after.previousPaymentDate,
6154 after.startDate.time_since_epoch().count()),
6157 auto const roundedInterest =
6160 asset.raw(), roundedInterest, managementFeeRate,
after.loanScale);
6161 Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee;
6165 env.
current()->parentCloseTime().time_since_epoch().count());
6167 after.startDate.time_since_epoch().count());
6168 auto const lastPaymentDate =
6170 auto const signedDelta =
static_cast<std::int64_t>(nowSecs) -
6172 auto const unsignedDelta =
6174 log <<
"PoC window: prev=" <<
after.previousPaymentDate
6175 <<
" start=" << startSecs <<
" now=" << nowSecs
6176 <<
" signedDelta=" << signedDelta
6177 <<
" unsignedDelta=" << unsignedDelta <<
std::endl;
6181 auto const prevClamped =
std::min(
after.previousPaymentDate, nowSecs);
6183 after.periodicPayment,
6185 after.paymentRemaining,
6186 env.
current()->parentCloseTime(),
6187 after.paymentInterval,
6192 asset.raw(), fullPaymentInterestClamped,
after.loanScale);
6195 roundedInterestClamped,
6198 Number const roundedFullInterestClamped =
6199 roundedInterestClamped - roundedFullMgmtFeeClamped;
6202 after.principalOutstanding + roundedFullInterestClamped +
6203 roundedFullMgmtFeeClamped + closePaymentFee};
6206 auto const vaultId2 =
6207 brokerSle2 ? brokerSle2->at(sfVaultID) :
uint256{};
6208 auto const vaultKey2 = keylet::vault(vaultId2);
6209 auto const vaultBefore = env.
le(vaultKey2);
6210 BEAST_EXPECT(vaultBefore);
6211 Number const assetsTotalBefore =
6212 vaultBefore ? vaultBefore->at(sfAssetsTotal) :
Number{};
6216 after.principalOutstanding + roundedFullInterest +
6217 roundedFullMgmtFee + closePaymentFee};
6219 log <<
"PoC payoff: principalOutstanding=" <<
after.principalOutstanding
6220 <<
" roundedFullInterest=" << roundedFullInterest
6221 <<
" roundedFullMgmtFee=" << roundedFullMgmtFee
6222 <<
" closeFee=" << closePaymentFee
6223 <<
" fullDue=" << to_string(fullDue.getJson()) <<
std::endl;
6224 log <<
"PoC reference (clamped): roundedFullInterestClamped="
6225 << roundedFullInterestClamped
6226 <<
" roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped
6227 <<
" fullDueClamped=" << to_string(fullDueClamped.getJson())
6235 BEAST_EXPECT(unsignedDelta >
after.paymentInterval);
6238 auto const vaultAfter = env.
le(vaultKey2);
6239 BEAST_EXPECT(vaultAfter);
6242 auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
6243 log <<
"PoC NAV: assetsTotalBefore=" << assetsTotalBefore
6244 <<
" assetsTotalAfter=" << assetsTotalAfter
6245 <<
" delta=" << (assetsTotalAfter - assetsTotalBefore)
6250 BEAST_EXPECT(fullDue == fullDueClamped);
6251 if (fullDue > fullDueClamped)
6252 log <<
"PoC delta: overcharge (fullDue > clamped)" <<
std::endl;
6256 auto const finalLoan = env.
le(loanKeylet);
6257 BEAST_EXPECT(finalLoan);
6260 BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0);
6261 BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0);
6268 testcase(
"Dust manipulation");
6270 using namespace jtx;
6271 using namespace std::chrono_literals;
6272 Env env(*
this, all);
6280 env.
fund(
XRP(1'000'000'00), issuer, lender, borrower, victim);
6284 auto asset = issuer[
"USD"];
6285 env(trust(lender, asset(100000)));
6286 env(trust(borrower, asset(100000)));
6287 env(trust(victim, asset(100000)));
6288 env(pay(issuer, lender, asset(50000)));
6289 env(pay(issuer, borrower, asset(50000)));
6290 env(pay(issuer, victim, asset(50000)));
6299 auto broker = createVaultAndBroker(env, asset, lender, brokerParams);
6302 auto const brokerSle = env.
le(keylet::loanbroker(broker.
brokerID));
6303 if (!BEAST_EXPECT(brokerSle))
6307 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
6311 auto const loanSequence = brokerSle->at(sfLoanSequence);
6312 return keylet::loan(broker.
brokerID, loanSequence);
6320 auto const vaultSle = env.
le(vaultKeylet);
6321 Number assetsTotal = vaultSle->at(sfAssetsTotal);
6322 Number assetsAvail = vaultSle->at(sfAssetsAvailable);
6324 log <<
"Before loan creation:" <<
std::endl;
6325 log <<
" AssetsTotal: " << assetsTotal <<
std::endl;
6326 log <<
" AssetsAvailable: " << assetsAvail <<
std::endl;
6327 log <<
" Difference: " << (assetsTotal - assetsAvail) <<
std::endl;
6330 BEAST_EXPECT(assetsAvail == assetsTotal);
6333 broker.
asset(brokerParams.vaultDeposit).number());
6336 Keylet const& loanKeylet = *loanKeyletOpt;
6340 .counter = borrower,
6341 .principalRequest =
Number{100},
6344 .payInterval = 86400 * 6,
6345 .gracePd = 86400 * 5,
6348 env(loanParams(env, broker));
6355 auto const vaultSle = env.
le(vaultKeylet);
6356 Number assetsTotal = vaultSle->at(sfAssetsTotal);
6357 Number assetsAvail = vaultSle->at(sfAssetsAvailable);
6359 log <<
"After loan creation:" <<
std::endl;
6360 log <<
" AssetsTotal: " << assetsTotal <<
std::endl;
6361 log <<
" AssetsAvailable: " << assetsAvail <<
std::endl;
6362 log <<
" Difference: " << (assetsTotal - assetsAvail) <<
std::endl;
6364 auto const loanSle = env.
le(loanKeylet);
6365 if (!BEAST_EXPECT(loanSle))
6370 log <<
" ValueOutstanding: " << state.valueOutstanding
6372 log <<
" PrincipalOutstanding: " << state.principalOutstanding
6374 log <<
" InterestOutstanding: " << state.interestOutstanding()
6376 log <<
" InterestDue: " << state.interestDue <<
std::endl;
6377 log <<
" FeeDue: " << state.managementFeeDue <<
std::endl;
6381 BEAST_EXPECT(assetsAvail < assetsTotal);
6390 broker.
asset(brokerParams.vaultDeposit + state.interestDue)
6400 auto const vaultSle2 = env.
le(vaultKeylet);
6401 Number assetsTotal2 = vaultSle2->at(sfAssetsTotal);
6402 Number assetsAvail2 = vaultSle2->at(sfAssetsAvailable);
6405 log <<
" AssetsTotal: " << assetsTotal2 <<
std::endl;
6406 log <<
" AssetsAvailable: " << assetsAvail2 <<
std::endl;
6407 log <<
" Difference: " << (assetsTotal2 - assetsAvail2)
6411 BEAST_EXPECT(assetsAvail2 == assetsTotal2);
6418 using namespace jtx;
6420 testcase(
"RIPD-3831");
6422 Account const issuer(
"issuer");
6423 Account const lender(
"lender");
6424 Account const borrower(
"borrower");
6434 .counter = borrower,
6435 .principalRequest =
Number{200'000, -6},
6436 .lateFee =
Number{200, -6},
6442 auto const assetType = AssetType::XRP;
6444 Env env(*
this, all);
6446 auto loanResult = createLoan(
6447 env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6449 if (!BEAST_EXPECT(loanResult))
6458 auto state = getCurrentState(env, broker, loanKeylet);
6459 if (
auto loan = env.
le(loanKeylet); BEAST_EXPECT(loan))
6465 loan->at(sfNextPaymentDueDate) + loan->at(sfGracePeriod) + 1}});
6469 env, broker, issuer, borrower, state, loanParams.
serviceFee);
6471 using namespace jtx::loan;
6474 pay(borrower, loanKeylet.key,
drops(
XRPAmount(state.totalValue)));
6477 auto const submitParam = to_string(jv);
6479 auto const jr = env.
rpc(
"submit", borrower.
name(), submitParam);
6482 BEAST_EXPECT(jr.isMember(jss::result));
6483 auto const jResult = jr[jss::result];
6493 env(
noop(borrower));
6504 testcase(
"RIPD-3459 - LoanBroker incorrect debt total");
6506 using namespace jtx;
6508 Account const issuer(
"issuer");
6509 Account const lender(
"lender");
6510 Account const borrower(
"borrower");
6520 .counter = borrower,
6521 .principalRequest =
Number{100'000, -4},
6526 auto const assetType = AssetType::MPT;
6528 Env env(*
this, all);
6530 auto loanResult = createLoan(
6531 env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6533 if (!BEAST_EXPECT(loanResult))
6543 BEAST_EXPECT(brokerSle))
6545 if (
auto const loanSle = env.
le(loanKeylet); BEAST_EXPECT(loanSle))
6548 brokerSle->at(sfDebtTotal) ==
6549 loanSle->at(sfTotalValueOutstanding));
6565 BEAST_EXPECT(brokerSle))
6567 if (
auto const loanSle = env.
le(loanKeylet); BEAST_EXPECT(loanSle))
6570 brokerSle->at(sfDebtTotal) ==
6571 loanSle->at(sfTotalValueOutstanding));
6572 BEAST_EXPECT(brokerSle->at(sfDebtTotal) == beast::zero);
6580 testcase(
"Crash with tfLoanOverpayment");
6581 using namespace jtx;
6582 using namespace loan;
6583 Account const lender{
"lender"};
6584 Account const issuer{
"issuer"};
6585 Account const borrower{
"borrower"};
6586 Account const depositor{
"depositor"};
6592 env.
fund(
XRP(10'000), lender, issuer, borrower, depositor);
6595 auto [tx, vaultKeyLet] =
6596 vault.create({.owner = lender, .asset =
xrpIssue()});
6601 {.depositor = depositor,
6602 .id = vaultKeyLet.key,
6603 .amount = XRP(1'000)}),
6607 auto const brokerKeyLet =
6608 keylet::loanbroker(lender.
id(), env.
seq(lender));
6610 env(loanBroker::set(lender, vaultKeyLet.key),
txfee);
6617 env(
set(borrower, brokerKeyLet.key, debtMaximumRequest),
6618 sig(sfCounterpartySignature, lender),
6621 paymentInterval(150),
6627 auto const loanKeylet = keylet::loan(brokerKeyLet.key, loanSequence);
6629 if (
auto loan = env.
le(loanKeylet); env.
test.BEAST_EXPECT(loan))
6631 env(loan::pay(borrower, loanKeylet.key,
XRPAmount(150'001)),
6641 testcase(
"Minimum cover rounding allows undercoverage (XRP)");
6643 using namespace jtx;
6644 using namespace loanBroker;
6646 Env env(*
this, all);
6648 Account const lender{
"lender"};
6649 Account const borrower{
"borrower"};
6651 env.
fund(
XRP(200'000), lender, borrower);
6656 auto [vaultCreate, vaultKeylet] =
6657 vault.create({.owner = lender, .asset =
xrpIssue()});
6660 BEAST_EXPECT(env.
le(vaultKeylet));
6672 auto const brokerInfo =
6673 createVaultAndBroker(env, xrpAsset, lender, brokerParams);
6676 env(loan::set(borrower, brokerInfo.brokerID, xrpAsset(804).value()),
6678 sig(sfCounterpartySignature, lender),
6684 if (
auto const brokerSle =
6685 env.
le(keylet::loanbroker(brokerInfo.brokerID));
6686 BEAST_EXPECT(brokerSle))
6689 BEAST_EXPECT(brokerSle->at(sfDebtTotal) ==
Number(804));
6694 env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(2).value()),
6701 env(coverWithdraw(lender, brokerInfo.brokerID, xrpAsset(1).value()));
6706 if (
auto const brokerSle =
6707 env.
le(keylet::loanbroker(brokerInfo.brokerID));
6708 BEAST_EXPECT(brokerSle))
6712 brokerSle->at(sfCoverAvailable) == xrpAsset(81).value());
6713 BEAST_EXPECT(brokerSle->at(sfDebtTotal) ==
Number(804));
6716 auto const theoreticalMin =
6718 log <<
"Theoretical min cover: " << theoreticalMin <<
std::endl;
6719 BEAST_EXPECT(
Number(804, -1) == theoreticalMin);
6726 testcase(
"RIPD-3902 - 1 IOU loan payments");
6728 using namespace jtx;
6730 Account const issuer(
"issuer");
6731 Account const lender(
"lender");
6732 Account const borrower(
"borrower");
6742 .counter = borrower,
6743 .principalRequest =
Number{1, 0},
6749 auto const assetType = AssetType::IOU;
6751 Env env(*
this, all);
6753 auto loanResult = createLoan(
6754 env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6756 if (!BEAST_EXPECT(loanResult))
6780 testcase(
"Test Borrower is Broker");
6781 using namespace jtx;
6782 using namespace loan;
6783 Account const broker{
"broker"};
6784 Account const issuer{
"issuer"};
6785 Account const borrower_{
"borrower"};
6786 Account const depositor{
"depositor"};
6788 auto testLoanAsset = [&](
auto&& getMaxDebt,
auto const& borrower) {
6792 if (borrower == broker)
6793 env.
fund(
XRP(10'000), broker, issuer, depositor);
6795 env.
fund(
XRP(10'000), broker, borrower, issuer, depositor);
6798 auto const xrpFee =
XRP(100);
6799 auto const txFee =
fee(xrpFee);
6801 STAmount const debtMaximumRequest = getMaxDebt(env);
6803 auto const& asset = debtMaximumRequest.
asset();
6804 auto const initialVault = asset(debtMaximumRequest * 100);
6806 auto [tx, vaultKeylet] =
6807 vault.create({.owner = broker, .
asset = asset});
6812 {.depositor = depositor,
6813 .id = vaultKeylet.key,
6814 .amount = initialVault}),
6818 auto const brokerKeylet =
6819 keylet::loanbroker(broker.id(), env.
seq(broker));
6821 env(loanBroker::set(broker, vaultKeylet.key), txFee);
6824 auto const serviceFee = 101;
6826 env(
set(broker, brokerKeylet.key, debtMaximumRequest),
6827 counterparty(borrower),
6828 sig(sfCounterpartySignature, borrower),
6829 loanServiceFee(serviceFee),
6835 auto const loanKeylet =
6836 keylet::loan(brokerKeylet.key, loanSequence);
6838 auto const brokerBalanceBefore = env.
balance(broker, asset);
6840 if (
auto const loanSle = env.
le(loanKeylet);
6841 env.
test.BEAST_EXPECT(loanSle))
6843 auto const payment = loanSle->at(sfPeriodicPayment);
6844 auto const totalPayment = payment + serviceFee;
6845 env(loan::pay(borrower, loanKeylet.key, asset(totalPayment)),
6848 if (
auto const vaultSle = env.
le(vaultKeylet);
6849 BEAST_EXPECT(vaultSle))
6851 auto const expected = [&]() {
6854 if (borrower != broker)
6855 return brokerBalanceBefore.number() + serviceFee;
6861 return brokerBalanceBefore.number() - payment -
6863 return brokerBalanceBefore.number() - payment;
6867 asset(expected).value());
6873 for (
auto const& borrowerAcct : {broker, borrower_})
6880 auto const IOU = issuer[
"USD"];
6881 env(trust(broker,
IOU(1'000'000'000)));
6882 env(trust(depositor,
IOU(1'000'000'000)));
6883 env(pay(issuer, broker,
IOU(100'000'000)));
6884 env(pay(issuer, depositor,
IOU(100'000'000)));
6886 return IOU(200'000);
6894 .holders = {broker, depositor},
6895 .pay = 100'000'000});
6896 return mpt(200'000);
6905 testcase(
"RIPD-4096 - Issuer as borrower");
6907 using namespace jtx;
6909 Account const issuer(
"issuer");
6910 Account const lender(
"lender");
6921 .principalRequest =
Number{10000}};
6923 auto const assetType = AssetType::IOU;
6925 Env env(*
this, all);
6927 auto loanResult = createLoan(
6928 env, assetType, brokerParams, loanParams, issuer, lender, issuer);
6930 if (!BEAST_EXPECT(loanResult))
6954 testcase(
"RIPD-4125 - overpayment");
6956 using namespace jtx;
6958 Account const issuer(
"issuer");
6959 Account const lender(
"lender");
6960 Account const borrower(
"borrower");
6970 .counter = borrower,
6971 .principalRequest =
Number{200000, -6},
6979 auto const assetType = AssetType::XRP;
6988 auto loanResult = createLoan(
6989 env, assetType, brokerParams, loanParams, issuer, lender, borrower);
6991 if (!BEAST_EXPECT(loanResult))
7000 auto const state = getCurrentState(env, broker, loanKeylet);
7005 STAmount{broker.asset, state.periodicPayment * 3 / 2 + 1},
7034 testLoanPayLateFullPaymentBypassesPenalties();
7035 testLoanCoverMinimumRoundingExploit();
7037 testCoverDepositWithdrawNonTransferableMPT();
7038 testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic();
7045 testServiceFeeOnBrokerDeepFreeze();
7050 testInvalidLoanDelete();
7051 testInvalidLoanManage();
7052 testInvalidLoanPay();
7053 testInvalidLoanSet();
7055 testBatchBypassCounterparty();
7056 testLoanPayComputePeriodicPaymentValidRateInvariant();
7057 testAccountSendMptMinAmountInvariant();
7058 testLoanPayDebtDecreaseInvariant();
7059 testWrongMaxDebtBehavior();
7060 testLoanPayComputePeriodicPaymentValidTotalInterestInvariant();
7062 testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant();
7063 testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant();
7064 testLoanNextPaymentDueDateOverflow();
7067 testDustManipulation();
7073 testRoundingAllowsUndercoverage();
7074 testBorrowerIsBroker();
7075 testIssuerIsBorrower();
7076 testLimitExceeded();
7115 using namespace jtx;
7117 Account const issuer(
"issuer");
7118 Account const lender(
"lender");
7119 Account const borrower(
"borrower");
7123 auto const principalRequest = principalDist(engine_);
7124 TenthBips16 managementFeeRate{managementFeeRateDist(engine_)};
7125 auto const serviceFee = serviceFeeDist(engine_);
7127 auto const payTotal = paymentTotalDist(engine_);
7128 auto const payInterval = paymentIntervalDist(engine_);
7134 .managementFeeRate = managementFeeRate};
7137 .counter = borrower,
7138 .principalRequest = principalRequest,
7139 .serviceFee = serviceFee,
7140 .interest = interest,
7141 .payTotal = payTotal,
7142 .payInterval = payInterval,
7145 runLoan(assetType, brokerParams, loanParams);
7152 auto const argument = arg();
7153 auto const numIterations = [s = arg()]() ->
int {
7160 auto const r = stoi(s, &pos);
7161 if (pos != s.size())
7171 using namespace jtx;
7173 auto const updateInterval =
std::min(numIterations / 5, 100);
7175 for (
int i = 0; i < numIterations; ++i)
7177 if (i % updateInterval == 0)
7178 testcase <<
"Random Loan Test iteration " << (i + 1) <<
"/"
7190 using namespace jtx;
7200 .counter =
Account(
"borrower"),
7201 .principalRequest =
Number{10000, 0},
7204 .payInterval = 150};
7206 runLoan(AssetType::XRP, brokerParams, loanParams);
7210BEAST_DEFINE_TESTSUITE(Loan, tx,
xrpl);
7211BEAST_DEFINE_TESTSUITE_MANUAL(LoanBatch, tx,
xrpl);
7212BEAST_DEFINE_TESTSUITE_MANUAL(LoanArbitrary, tx,
xrpl);
Lightweight wrapper to tag static string.
Value removeMember(char const *key)
Remove and return the named member.
log_os< char > log
Logging output stream.
testcase_t testcase
Memberspace for declaring test cases.
virtual LoadFeeTrack & getFeeTrack()=0
constexpr TIss const & get() const
constexpr bool holds() const
constexpr value_type const & value() const
static constexpr auto disabledTxTypes
A currency issued by an account.
static std::uint32_t constexpr minPaymentInterval
static std::uint32_t constexpr minPaymentTotal
static std::uint32_t constexpr defaultPaymentInterval
static std::uint32_t constexpr defaultPaymentTotal
std::chrono::time_point< NetClock > time_point
std::chrono::duration< rep, period > duration
Number truncate() const noexcept
constexpr int exponent() const noexcept
Slice slice() const noexcept
Asset const & asset() const
STAmount const & value() const noexcept
Blob getFieldVL(SField const &field) const
Serializer getSerializer() const
void setFieldObject(SField const &field, STObject const &v)
STObject getFieldObject(SField const &field) const
Slice slice() const noexcept
An immutable linear range of bytes.
constexpr value_type drops() const
Returns the number of drops.
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
void testBorrowerIsBroker()
void testLoanPayDebtDecreaseInvariant()
void testWrongMaxDebtBehavior()
void testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant()
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 testInvalidLoanPay()
void testRoundingAllowsUndercoverage()
void testLoanPayComputePeriodicPaymentValidTotalInterestInvariant()
void testCoverDepositWithdrawNonTransferableMPT()
void testInvalidLoanDelete()
void testPoC_UnsignedUnderflowOnFullPayAfterEarlyPeriodic()
void testLoanPayComputePeriodicPaymentValidRateInvariant()
void runLoan(AssetType assetType, BrokerParameters const &brokerParams, LoanParameters const &loanParams)
void testInvalidLoanSet()
void testIssuerIsBorrower()
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 testInvalidLoanManage()
void testLoanNextPaymentDueDateOverflow()
void testDustManipulation()
void testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant()
BrokerInfo createVaultAndBroker(jtx::Env &env, jtx::PrettyAsset const &asset, jtx::Account const &lender, BrokerParameters const ¶ms=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 testBatchBypassCounterparty()
void testServiceFeeOnBrokerDeepFreeze()
Immutable cryptographic account descriptor.
SecretKey const & sk() const
Return the secret key.
std::string const & human() const
Returns the human readable public key.
std::string const & name() const
Return the name.
PublicKey const & pk() const
Return the public key.
AccountID id() const
Returns the Account ID.
A transaction testing environment.
bool close(NetClock::time_point closeTime, std::optional< std::chrono::milliseconds > consensusDelay=std::nullopt)
Close and advance the ledger.
TER ter() const
Return the TER for the last JTx.
std::uint32_t ownerCount(Account const &account) const
Return the number of objects owned by an account.
std::shared_ptr< SLE const > le(Account const &account) const
Return an account root.
void fund(bool setDefaultRipple, STAmount const &amount, Account const &account)
std::uint32_t seq(Account const &account) const
Returns the next sequence number on account.
JTx jt(JsonValue &&jv, FN const &... fN)
Create a JTx from parameters.
PrettyAmount balance(Account const &account) const
Returns the XRP balance on an account.
Json::Value json(JsonValue &&jv, FN const &... fN)
Create JSON from parameters.
beast::unit_test::suite & test
void trust(STAmount const &amount, Account const &account)
Establish trust lines.
Json::Value rpc(unsigned apiVersion, std::unordered_map< std::string, std::string > const &headers, std::string const &cmd, Args &&... args)
Execute an RPC command.
std::shared_ptr< STObject const > meta()
Return metadata for the last JTx.
beast::Journal const journal
void require(Args const &... args)
Check a set of requirements.
std::shared_ptr< OpenView const > current() const
Returns the current ledger.
NetClock::time_point now()
Returns the current network time.
Converts to IOU Issue or STAmount.
void set(MPTSet const &set={})
void authorize(MPTAuthorize const &arg=MPTAuthorize{})
MPTID const & issuanceID() const
Converts to MPT Issue or STAmount.
Adds a new Batch Txn on a JTx and autofills.
Set a multisignature on a JTx.
Set the regular signature on a JTx.
Set the expected result code for a JTx The test will fail if the code doesn't match.
constexpr value_type value() const
Returns the underlying value.
T emplace_back(T... args)
@ objectValue
object value (collection of name/value pairs).
beast::abstract_clock< std::chrono::steady_clock > clock_type
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
Keylet loan(uint256 const &loanBrokerID, std::uint32_t loanSeq) noexcept
Keylet vault(AccountID const &owner, std::uint32_t seq) noexcept
Keylet line(AccountID const &id0, AccountID const &id1, Currency const ¤cy) noexcept
The index of a trust line for a given currency.
Keylet mptoken(MPTID const &issuanceID, AccountID const &holder) noexcept
Keylet account(AccountID const &id) noexcept
AccountID root.
Json::Value trust(AccountID const &account, STAmount const &amount, std::uint32_t flags=0)
Json::Value pay(Account const &account, AccountID const &to, STAmount const &amount)
Json::Value outer(jtx::Account const &account, uint32_t seq, STAmount const &fee, std::uint32_t flags)
Batch.
XRPAmount calcBatchFee(jtx::Env const &env, uint32_t const &numSigners, uint32_t const &txns=0)
Calculate Batch Fee.
Keylet keylet(test::jtx::Account const &subject, test::jtx::Account const &issuer, std::string_view credType)
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 managementFeeRate
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
auto const data
General field definitions, or fields used in multiple transaction namespaces.
Json::Value noop(Account const &account)
The null transaction.
Json::Value trust(Account const &account, STAmount const &amount, std::uint32_t flags)
Modify a trust line.
Json::Value signers(Account const &account, std::uint32_t quorum, std::vector< signer > const &v)
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.
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)
Json::Value pay(AccountID const &account, AccountID const &to, AnyAmount amount)
Create a payment.
Json::Value fclear(Account const &account, std::uint32_t off)
Remove account flag.
FeatureBitset testable_amendments()
Json::Value fset(Account const &account, std::uint32_t on, std::uint32_t off=0)
Add and/or remove flag.
PrettyAmount drops(Integer i)
Returns an XRP PrettyAmount, which is trivially convertible to STAmount.
static disabled_t const disabled
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.
bool set(T &target, std::string const &name, Section const §ion)
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
Issue const & xrpIssue()
Returns an asset specifier that represents XRP.
Number loanPeriodicRate(TenthBips32 interestRate, std::uint32_t paymentInterval)
constexpr std::uint32_t const tmfMPTClearCanTransfer
Number computeManagementFee(Asset const &asset, Number const &value, TenthBips32 managementFeeRate, std::int32_t scale)
int run(int argc, char **argv)
constexpr std::uint32_t const tfMPTCanTransfer
constexpr std::uint32_t const tmfMPTCanMutateCanTransfer
std::string to_string(base_uint< Bits, Tag > const &a)
std::string strHex(FwdIt begin, FwdIt end)
static constexpr Number numZero
constexpr TenthBips32 percentageToTenthBips(std::uint32_t percentage)
constexpr std::uint32_t const tfMPTRequireAuth
constexpr std::uint32_t const tfLoanImpair
Number roundToAsset(A const &asset, Number const &value, std::int32_t scale, Number::rounding_mode rounding=Number::getround())
Round an arbitrary precision Number to the precision of a given Asset.
constexpr T tenthBipsOfValue(T value, TenthBips< TBips > bips)
constexpr std::uint32_t const tfLoanFullPayment
static constexpr std::uint32_t secondsInYear
constexpr std::uint32_t const tfLoanOverpayment
std::size_t constexpr maxDataPayloadLength
The maximum length of Data payload.
TenthBips< std::uint32_t > TenthBips32
constexpr std::uint32_t const tfMPTUnlock
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
constexpr std::uint32_t const tfMPTCanLock
@ current
This was a new validation and was added.
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
constexpr std::uint32_t const tfMPTCanClawback
constexpr std::uint32_t tfClearFreeze
constexpr std::uint32_t const tfMPTLock
constexpr std::uint32_t tfFullyCanonicalSig
Transaction flags.
constexpr std::uint32_t tfClearDeepFreeze
bool after(NetClock::time_point now, std::uint32_t mark)
Has the specified time passed?
constexpr std::uint32_t tfSetDeepFreeze
TER requireAuth(ReadView const &view, Issue const &issue, AccountID const &account, AuthType authType=AuthType::Legacy)
Check if the account lacks required authorization.
constexpr std::uint32_t const tfLoanSetMask
constexpr XRPAmount DROPS_PER_XRP
Number of drops per 1 XRP.
constexpr std::uint32_t const tfMPTUnauthorize
TenthBips32 constexpr tenthBipsPerUnity(bipsPerUnity.value() *10)
constexpr std::uint32_t const tfLoanDefault
constexpr Number abs(Number x) noexcept
int getVaultScale(SLE::const_ref vaultSle)
constexpr std::uint32_t tfSetfAuth
TenthBips< std::uint16_t > TenthBips16
constexpr std::uint32_t asfRequireAuth
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
@ txSign
inner transaction to sign
LoanState constructRoundedLoanState(SLE::const_ref loan)
Buffer sign(PublicKey const &pk, SecretKey const &sk, Slice const &message)
Generate a signature for a message.
constexpr std::uint32_t const tfLoanUnimpair
@ tecINSUFFICIENT_RESERVE
@ tecINSUFFICIENT_PAYMENT
LoanProperties computeLoanProperties(Asset const &asset, Number principalOutstanding, TenthBips32 interestRate, std::uint32_t paymentInterval, std::uint32_t paymentsRemaining, TenthBips32 managementFeeRate, std::int32_t minimumScale)
constexpr std::uint32_t tfSetFreeze
LoanState constructLoanState(Number const &totalValueOutstanding, Number const &principalOutstanding, Number const &managementFeeOutstanding)
Number computeFullPaymentInterest(Number const &rawPrincipalOutstanding, Number const &periodicRate, NetClock::time_point parentCloseTime, std::uint32_t paymentInterval, std::uint32_t prevPaymentDate, std::uint32_t startDate, TenthBips32 closeInterestRate)
LoanState computeRawLoanState(Number const &periodicPayment, Number const &periodicRate, std::uint32_t const paymentRemaining, TenthBips32 const managementFeeRate)
constexpr std::uint32_t const tmfMPTSetCanTransfer
constexpr std::uint32_t const tfLoanManageMask
bool isRounded(Asset const &asset, Number const &value, std::int32_t scale)
A pair of SHAMap key and LedgerEntryType.
This structure captures the parts of a loan state.
Number principalOutstanding
int vaultScale(jtx::Env const &env) const
Keylet vaultKeylet() const
BrokerInfo(jtx::PrettyAsset const &asset_, Keylet const &brokerKeylet_, Keylet const &vaultKeylet_, BrokerParameters const &p)
Keylet brokerKeylet() const
Number maxCoveredLoanValue(Number const ¤tDebt) const
static BrokerParameters const & defaults()
TenthBips16 managementFeeRate
TenthBips32 coverRateLiquidation
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
bool counterpartyExplicit
std::optional< std::uint32_t > gracePd
std::optional< TenthBips32 > overFee
std::optional< TenthBips32 > closeInterest
std::optional< Number > originationFee
std::uint32_t previousPaymentDate
TenthBips32 const interestRate
NetClock::time_point startDate
std::uint32_t const paymentInterval
std::uint32_t paymentRemaining
std::int32_t const loanScale
Number principalOutstanding
std::uint32_t nextPaymentDate
Number managementFeeOutstanding
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.
BrokerInfo const & broker
jtx::Account const & pseudoAccount
Keylet const & loanKeylet
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.
std::shared_ptr< STTx const > stx
std::optional< MPTCreate > create
Represents an XRP or IOU quantity This customizes the string conversion and supports XRP conversions ...
STAmount const & value() const
Asset const & raw() const
Set the sequence number on a JTx.
T time_since_epoch(T... args)