761 using namespace jtx::loan;
762 using namespace std::chrono_literals;
765 bool const showStepBalances = paymentParams.showStepBalances;
769 auto const baseFee = env.
current()->fees().base;
774 verifyLoanStatus(state);
788 auto const periodicRate =
loanPeriodicRate(state.interestRate, state.paymentInterval);
789 STAmount const roundedPeriodicPayment{
792 if (!showStepBalances)
793 log << currencyLabel <<
" Payment components: "
794 <<
"Payments remaining, "
795 <<
"rawInterest, rawPrincipal, "
797 <<
"trackedValueDelta, trackedPrincipalDelta, "
798 "trackedInterestDelta, trackedMgmtFeeDelta, special"
804 auto currentRoundedState =
805 constructLoanState(state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
810 if (showStepBalances)
812 log << currencyLabel <<
" Starting loan balances: "
813 <<
"\n\tTotal value: " << currentRoundedState.valueOutstanding
814 <<
"\n\tPrincipal: " << currentRoundedState.principalOutstanding
815 <<
"\n\tInterest: " << currentRoundedState.interestDue
816 <<
"\n\tMgmt fee: " << currentRoundedState.managementFeeDue <<
"\n\tPayments remaining "
821 log << currencyLabel <<
" Loan starting state: " << state.paymentRemaining <<
", " << raw.interestDue
822 <<
", " << raw.principalOutstanding <<
", " << raw.managementFeeDue <<
", "
823 << currentRoundedState.valueOutstanding <<
", " << currentRoundedState.principalOutstanding <<
", "
824 << currentRoundedState.interestDue <<
", " << currentRoundedState.managementFeeDue <<
std::endl;
830 auto const extraAmount = paymentParams.overpaymentExtra
831 ? broker.
asset(*paymentParams.overpaymentExtra).value()
835 STAmount{broker.
asset, totalDue * paymentParams.overpaymentFactor} + extraAmount;
838 auto const initialState = state;
840 .
trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0};
841 Number totalInterestPaid = 0;
848 auto validateBorrowerBalance = [&]() {
849 if (borrower == issuer || !paymentParams.validateBalances)
851 auto const totalSpent =
852 (totalPaid.trackedValueDelta + totalFeesPaid +
854 BEAST_EXPECT(env.
balance(borrower, broker.
asset).
number() == borrowerInitialBalance - totalSpent);
859 auto const p = places.value_or(defaultRound);
862 auto const factor =
Number{1, p};
863 return (n * factor).
truncate() / factor;
865 while (state.paymentRemaining > 0)
867 validateBorrowerBalance();
873 state.principalOutstanding,
874 state.managementFeeOutstanding,
875 state.periodicPayment,
877 state.paymentRemaining,
881 paymentComponents.trackedValueDelta <= roundedPeriodicPayment ||
883 paymentComponents.trackedValueDelta >= roundedPeriodicPayment));
885 paymentComponents.trackedValueDelta ==
886 paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() +
887 paymentComponents.trackedManagementFeeDelta);
895 deltas.
total() == state.periodicPayment ||
896 (state.loanScale - (deltas.
total() - state.periodicPayment).exponent()) > 14);
898 if (!showStepBalances)
899 log << currencyLabel <<
" Payment components: " << state.paymentRemaining <<
", "
902 << paymentComponents.trackedValueDelta <<
", " << paymentComponents.trackedPrincipalDelta <<
", "
903 << paymentComponents.trackedInterestPart() <<
", " << paymentComponents.trackedManagementFeeDelta
910 auto const totalDueAmount =
STAmount{broker.
asset, paymentComponents.trackedValueDelta + serviceFee};
912 if (paymentParams.validateBalances)
920 Number const diff = totalDue - totalDueAmount;
923 (diff > beast::zero &&
925 (state.loanScale - diff.
exponent() > 13))));
928 paymentComponents.trackedPrincipalDelta >= beast::zero &&
929 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
932 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
935 auto const borrowerBalanceBeforePayment = env.
balance(borrower, broker.
asset);
938 env(
pay(borrower, loanKeylet.
key, transactionAmount, paymentParams.flags));
940 env.
close(d{state.paymentInterval / 2});
942 if (paymentParams.validateBalances)
948 adjustment = env.
current()->fees().base;
953 state.loanScale, borrower, borrowerBalanceBeforePayment, totalDueAmount, adjustment);
956 if (showStepBalances)
958 auto const loanSle = env.
le(loanKeylet);
959 if (!BEAST_EXPECT(loanSle))
963 auto const errors = nextTrueState -
current;
964 log << currencyLabel <<
" Loan balances: "
965 <<
"\n\tAmount taken: " << paymentComponents.trackedValueDelta
966 <<
"\n\tTotal value: " <<
current.valueOutstanding
967 <<
" (true: " << truncate(nextTrueState.
valueOutstanding) <<
", error: " << truncate(errors.total())
968 <<
")\n\tPrincipal: " <<
current.principalOutstanding
970 <<
", error: " << truncate(errors.principal) <<
")\n\tInterest: " <<
current.interestDue
971 <<
" (true: " << truncate(nextTrueState.
interestDue) <<
", error: " << truncate(errors.interest)
972 <<
")\n\tMgmt fee: " <<
current.managementFeeDue
974 <<
", error: " << truncate(errors.managementFee) <<
")\n\tPayments remaining "
975 << loanSle->at(sfPaymentRemaining) <<
std::endl;
980 --state.paymentRemaining;
981 state.previousPaymentDate = state.nextPaymentDate;
984 state.paymentRemaining = 0;
985 state.nextPaymentDate = 0;
989 state.nextPaymentDate += state.paymentInterval;
991 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
992 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
993 state.totalValue -= paymentComponents.trackedValueDelta;
995 if (paymentParams.validateBalances)
996 verifyLoanStatus(state);
998 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
999 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
1000 totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta;
1001 totalInterestPaid += paymentComponents.trackedInterestPart();
1002 totalFeesPaid += serviceFee;
1003 ++totalPaymentsMade;
1005 currentTrueState = nextTrueState;
1007 validateBorrowerBalance();
1010 BEAST_EXPECT(state.paymentRemaining == 0);
1011 BEAST_EXPECT(state.principalOutstanding == 0);
1013 auto const initialInterestDue =
1014 initialState.totalValue - (initialState.principalOutstanding + initialState.managementFeeOutstanding);
1015 if (paymentParams.validateBalances)
1018 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
1019 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
1020 BEAST_EXPECT(totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
1023 BEAST_EXPECT(totalInterestPaid == initialInterestDue);
1024 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
1027 if (showStepBalances)
1029 auto const loanSle = env.
le(loanKeylet);
1030 if (!BEAST_EXPECT(loanSle))
1033 log << currencyLabel <<
" Total amounts paid: "
1034 <<
"\n\tTotal value: " << totalPaid.trackedValueDelta
1035 <<
" (initial: " << truncate(initialState.totalValue)
1036 <<
", error: " << truncate(initialState.totalValue - totalPaid.trackedValueDelta)
1037 <<
")\n\tPrincipal: " << totalPaid.trackedPrincipalDelta
1038 <<
" (initial: " << truncate(initialState.principalOutstanding)
1039 <<
", error: " << truncate(initialState.principalOutstanding - totalPaid.trackedPrincipalDelta)
1040 <<
")\n\tInterest: " << totalInterestPaid <<
" (initial: " << truncate(initialInterestDue)
1041 <<
", error: " << truncate(initialInterestDue - totalInterestPaid)
1042 <<
")\n\tMgmt fee: " << totalPaid.trackedManagementFeeDelta
1043 <<
" (initial: " << truncate(initialState.managementFeeOutstanding)
1044 <<
", error: " << truncate(initialState.managementFeeOutstanding - totalPaid.trackedManagementFeeDelta)
1045 <<
")\n\tTotal payments made: " << totalPaymentsMade <<
std::endl;
1097 Number const& loanAmount,
1098 int interestExponent,
1109 auto const [keylet, loanSequence] = [&]() {
1111 if (!BEAST_EXPECT(brokerSle))
1116 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1120 auto const loanSequence = brokerSle->at(sfLoanSequence);
1129 if (!BEAST_EXPECT(loanSequence != 0))
1132 testcase << caseLabel <<
" " << label;
1134 using namespace jtx;
1135 using namespace loan;
1136 using namespace std::chrono_literals;
1138 auto applyExponent = [interestExponent,
this](
TenthBips32 value)
mutable {
1140 while (interestExponent > 0)
1142 auto const oldValue = value;
1145 BEAST_EXPECT(value / 10 == oldValue);
1147 while (interestExponent < 0)
1149 auto const oldValue = value;
1152 BEAST_EXPECT(value * 10 == oldValue);
1157 auto const borrowerOwnerCount = env.
ownerCount(borrower);
1159 auto const loanSetFee = env.
current()->fees().base * 2;
1163 .counterpartyExplicit =
false,
1164 .principalRequest = loanAmount,
1165 .setFee = loanSetFee,
1166 .originationFee = 1,
1183 auto const serviceFeeAmount = broker.
asset(*loanParams.
serviceFee).value();
1184 auto const lateFeeAmount = broker.
asset(*loanParams.
lateFee).value();
1185 auto const closeFeeAmount = broker.
asset(*loanParams.
closeFee).value();
1187 auto const borrowerStartbalance = env.
balance(borrower, broker.
asset);
1189 auto createJtx = loanParams(env, broker);
1195 auto const startDate = env.
current()->header().parentCloseTime.time_since_epoch().count();
1199 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1);
1207 adjustment = 2 * env.
current()->fees().base;
1212 borrowerStartbalance.
value() + principalRequestAmount - originationFeeAmount - adjustment.
value());
1217 if (
auto loan = env.
le(keylet); BEAST_EXPECT(loan))
1222 BEAST_EXPECT(loan->at(sfLoanSequence) == loanSequence);
1223 BEAST_EXPECT(loan->at(sfBorrower) == borrower.
id());
1224 BEAST_EXPECT(loan->at(sfLoanBrokerID) == broker.
brokerID);
1225 BEAST_EXPECT(loan->at(sfLoanOriginationFee) == originationFeeAmount);
1226 BEAST_EXPECT(loan->at(sfLoanServiceFee) == serviceFeeAmount);
1227 BEAST_EXPECT(loan->at(sfLatePaymentFee) == lateFeeAmount);
1228 BEAST_EXPECT(loan->at(sfClosePaymentFee) == closeFeeAmount);
1229 BEAST_EXPECT(loan->at(sfOverpaymentFee) == *loanParams.
overFee);
1230 BEAST_EXPECT(loan->at(sfInterestRate) == *loanParams.
interest);
1231 BEAST_EXPECT(loan->at(sfLateInterestRate) == *loanParams.
lateInterest);
1232 BEAST_EXPECT(loan->at(sfCloseInterestRate) == *loanParams.
closeInterest);
1234 BEAST_EXPECT(loan->at(sfStartDate) == startDate);
1235 BEAST_EXPECT(loan->at(sfPaymentInterval) == *loanParams.
payInterval);
1236 BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.
gracePd);
1237 BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0);
1238 BEAST_EXPECT(loan->at(sfNextPaymentDueDate) == startDate + *loanParams.
payInterval);
1239 BEAST_EXPECT(loan->at(sfPaymentRemaining) == *loanParams.
payTotal);
1241 loan->at(sfLoanScale) >=
1243 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequestAmount);
1250 state.principalOutstanding,
1252 state.paymentInterval,
1253 state.paymentRemaining,
1262 loanProperties.loanState.valueOutstanding,
1263 principalRequestAmount,
1264 loanProperties.loanState.managementFeeDue,
1265 loanProperties.periodicPayment,
1270 env(manage(lender, keylet.
key, 0));
1273 auto jt = manage(lender, keylet.
key, 0);
1274 jt.removeMember(sfFlags.getName());
1301 auto const nextDueDate = startDate + *loanParams.
payInterval;
1309 loanProperties.loanScale,
1310 loanProperties.loanState.valueOutstanding,
1311 principalRequestAmount,
1312 loanProperties.loanState.managementFeeDue,
1313 loanProperties.periodicPayment,
1319 if (BEAST_EXPECT(toEndOfLife))
1320 toEndOfLife(keylet, verifyLoanStatus);
1324 if (
auto loan = env.le(keylet); BEAST_EXPECT(loan))
1326 BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0);
1327 BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0);
1329 auto const borrowerStartingBalance = env.balance(borrower, broker.
asset);
1345 static unsigned deleteCounter = 0;
1346 auto const deleter = ++deleteCounter % 2 ? lender : borrower;
1347 env(del(deleter, keylet.
key));
1351 if (deleter == borrower)
1356 adjustment = env.current()->fees().base;
1363 BEAST_EXPECT(env.balance(borrower, broker.
asset).value() == borrowerStartingBalance.value() - adjustment);
1364 BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount);
1368 BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 0);
1393 Number const& loanAmount,
1394 int interestExponent)
1396 using namespace jtx;
1397 using namespace Lending;
1399 auto const& asset = broker.
asset.
raw();
1401 auto const caseLabel = [&]() {
1403 ss <<
"Lifecycle: " << loanAmount <<
" " << currencyLabel <<
" Scale interest to: " << interestExponent
1409 using namespace loan;
1410 using namespace std::chrono_literals;
1414 Account const issuer{
"issuer"};
1417 Account const lender{
"lender"};
1419 Account const borrower{
"borrower"};
1425 Number const principalRequest = broker.
asset(loanAmount).value();
1427 BEAST_EXPECT(maxCoveredLoanValue == 1000 * 100 / 10);
1428 Number const maxCoveredLoanRequest = broker.
asset(maxCoveredLoanValue).value();
1432 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
1434 auto const pseudoAcct = [&]() {
1436 if (!BEAST_EXPECT(brokerSle))
1438 auto const brokerPseudo = brokerSle->at(sfAccount);
1439 return Account(
"Broker pseudo-account", brokerPseudo);
1442 auto const baseFee = env.
current()->fees().base;
1448 sig(sfCounterpartySignature, lender),
1455 sig(sfCounterpartySignature, borrower),
1461 sig(sfCounterpartySignature, lender),
1469 sig(sfCounterpartySignature, borrower),
1470 overpaymentFee(maxOverpaymentFee),
1475 sig(sfCounterpartySignature, lender),
1476 overpaymentFee(maxOverpaymentFee + 1),
1482 sig(sfCounterpartySignature, borrower),
1483 interestRate(maxInterestRate),
1487 sig(sfCounterpartySignature, borrower),
1493 sig(sfCounterpartySignature, lender),
1494 interestRate(maxInterestRate + 1),
1499 sig(sfCounterpartySignature, lender),
1506 sig(sfCounterpartySignature, borrower),
1507 lateInterestRate(maxLateInterestRate),
1511 sig(sfCounterpartySignature, borrower),
1517 sig(sfCounterpartySignature, lender),
1518 lateInterestRate(maxLateInterestRate + 1),
1523 sig(sfCounterpartySignature, lender),
1530 sig(sfCounterpartySignature, borrower),
1531 closeInterestRate(maxCloseInterestRate),
1535 sig(sfCounterpartySignature, borrower),
1541 sig(sfCounterpartySignature, lender),
1542 closeInterestRate(maxCloseInterestRate + 1),
1546 sig(sfCounterpartySignature, lender),
1553 sig(sfCounterpartySignature, borrower),
1554 overpaymentInterestRate(maxOverpaymentInterestRate),
1558 sig(sfCounterpartySignature, borrower),
1564 sig(sfCounterpartySignature, lender),
1565 overpaymentInterestRate(maxOverpaymentInterestRate + 1),
1569 sig(sfCounterpartySignature, lender),
1576 sig(sfCounterpartySignature, borrower),
1582 sig(sfCounterpartySignature, lender),
1589 sig(sfCounterpartySignature, borrower),
1595 sig(sfCounterpartySignature, lender),
1602 sig(sfCounterpartySignature, borrower),
1609 sig(sfCounterpartySignature, lender),
1616 env(
set(borrower, broker.
brokerID, principalRequest),
1617 sig(sfCounterpartySignature, lender),
1620 env(
signers(lender, 2, {{evan, 1}, {borrower, 1}}));
1621 env(
signers(borrower, 2, {{evan, 1}, {lender, 1}}));
1622 env(
set(borrower, broker.
brokerID, principalRequest),
1623 counterparty(lender),
1625 msig(sfCounterpartySignature, evan, borrower),
1629 env(
set(borrower, broker.
brokerID, principalRequest),
1630 counterparty(lender),
1631 msig(alice, issuer),
1632 msig(sfCounterpartySignature, evan, borrower),
1636 env(
set(borrower, broker.
brokerID, principalRequest),
1637 counterparty(lender),
1639 msig(sfCounterpartySignature, alice, issuer),
1645 env(
set(borrower, broker.
brokerID, principalRequest),
1646 counterparty(lender),
1648 msig(sfCounterpartySignature, evan, borrower),
1653 env(
set(borrower, broker.
brokerID, principalRequest),
1654 sig(sfCounterpartySignature, evan),
1659 counterparty(borrower),
1660 sig(sfCounterpartySignature, borrower),
1664 env(
set(lender, badKeylet.key, principalRequest),
1665 sig(sfCounterpartySignature, evan),
1669 env(
set(lender, badKeylet.key, principalRequest),
1670 counterparty(borrower),
1671 sig(sfCounterpartySignature, borrower),
1675 env(
set(lender, broker.
brokerID, principalRequest),
1676 counterparty(alice),
1677 sig(sfCounterpartySignature, alice),
1682 env(
set(evan, broker.
brokerID, totalVaultRequest + 1),
1683 sig(sfCounterpartySignature, lender),
1689 env(
set(evan, broker.
brokerID, maxCoveredLoanRequest + 1),
1690 sig(sfCounterpartySignature, lender),
1699 if (!BEAST_EXPECT(brokerSle))
1702 auto const vaultPseudo = [&]() {
1703 auto const vaultSle = env.
le(
keylet::vault(brokerSle->at(sfVaultID)));
1704 if (!BEAST_EXPECT(vaultSle))
1707 auto const vaultPseudo =
Account(
"Vault pseudo-account", vaultSle->at(sfAccount));
1711 auto const [
freeze, deepfreeze, unfreeze, expectedResult] =
1729 auto deepfreeze = [&](
Account const& holder) {
1732 auto unfreeze = [&](
Account const& holder) {
1740 mptt.
set({.account = issuer, .holder = holder, .flags =
tfMPTLock});
1742 auto unfreeze = [&](
Account const& holder) {
1743 mptt.
set({.account = issuer, .holder = holder, .flags =
tfMPTUnlock});
1752 for (
auto const& account : {vaultPseudo, evan})
1758 env(
set(evan, broker.
brokerID, debtMaximumRequest),
1759 sig(sfCounterpartySignature, lender),
1761 ter(expectedResult));
1764 BEAST_EXPECT(unfreeze);
1770 env(
set(evan, broker.
brokerID, debtMaximumRequest + 1),
1771 sig(sfCounterpartySignature, lender),
1787 for (
auto const& account : {
1795 deepfreeze(account);
1798 env(
set(evan, broker.
brokerID, debtMaximumRequest),
1799 sig(sfCounterpartySignature, lender),
1801 ter(expectedResult));
1804 BEAST_EXPECT(unfreeze);
1810 env(
set(evan, broker.
brokerID, debtMaximumRequest + 1),
1811 sig(sfCounterpartySignature, lender),
1821 auto coverAvailable = [&env,
this](
uint256 const& brokerID,
Number const& expected) {
1824 auto const available = brokerSle->
at(sfCoverAvailable);
1844 state.totalValue - state.managementFeeOutstanding),
1850 auto replenishCover = [&env, &coverAvailable](
1853 Number const& startingCoverAvailable,
1854 Number const& amountToBeCovered) {
1855 coverAvailable(broker.
brokerID, startingCoverAvailable - amountToBeCovered);
1857 coverAvailable(broker.
brokerID, startingCoverAvailable);
1861 auto defaultImmediately = [&](
std::uint32_t baseFlag,
bool impair =
true) {
1868 auto state =
getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
1869 BEAST_EXPECT(state.flags == baseFlag);
1871 auto const& broker = verifyLoanStatus.
broker;
1872 auto const startingCoverAvailable =
1891 verifyLoanStatus(state);
1894 auto const nextDueDate = tp{d{state.nextPaymentDate}};
1902 env.
close(nextDueDate + 60s);
1904 auto const [amountToBeCovered, brokerAcct] = getDefaultInfo(state, broker);
1912 replenishCover(broker, brokerAcct, startingCoverAvailable, amountToBeCovered);
1915 state.paymentRemaining = 0;
1916 state.totalValue = 0;
1917 state.principalOutstanding = 0;
1918 state.managementFeeOutstanding = 0;
1919 state.nextPaymentDate = 0;
1920 verifyLoanStatus(state);
1930 auto singlePayment = [&](
Keylet const& loanKeylet,
1939 verifyLoanStatus(state);
1958 STAmount{broker.asset, state.periodicPayment * Number{15, -1}},
1968 broker.
asset(state.periodicPayment * 2),
1973 broker.
asset(state.periodicPayment * 2),
1978 broker.
asset(state.periodicPayment * 2),
1983 broker.
asset(state.periodicPayment * 2),
1988 auto const otherAsset = broker.
asset.
raw() == assets[0].raw() ? assets[1] : assets[0];
1997 auto const borrowerBalanceBeforePayment = env.
balance(borrower, broker.
asset);
1999 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2002 auto const transactionAmount = payoffAmount + broker.
asset(10);
2008 (borrowerBalanceBeforePayment.number() * 2 / state.periodicPayment / loanPaymentsPerFeeIncrement + 1)};
2011 STAmount{broker.asset, borrowerBalanceBeforePayment.number() * 2},
2016 XRPAmount const goodFee{baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)};
2017 env(
pay(borrower, loanKeylet.
key, transactionAmount, txFlags),
fee(goodFee));
2027 adjustment = badFee + goodFee;
2030 state.paymentRemaining = 0;
2031 state.principalOutstanding = 0;
2032 state.totalValue = 0;
2033 state.managementFeeOutstanding = 0;
2034 state.previousPaymentDate = state.nextPaymentDate + state.paymentInterval * (numPayments - 1);
2035 state.nextPaymentDate = 0;
2036 verifyLoanStatus(state);
2038 verifyLoanStatus.checkPayment(
2039 state.loanScale, borrower, borrowerBalanceBeforePayment, payoffAmount, adjustment);
2047 return [&, baseFlag](
Keylet const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
2050 auto state =
getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2051 env.
close(state.startDate + 20s);
2052 auto const loanAge = (env.
now() - state.startDate).count();
2053 BEAST_EXPECT(loanAge == 30);
2063 Number const interval = state.paymentInterval;
2066 STAmount const principalOutstanding{broker.
asset, state.principalOutstanding};
2067 STAmount
const accruedInterest{
2068 broker.
asset, state.principalOutstanding * periodicRate * loanAge / interval};
2069 BEAST_EXPECT(accruedInterest == broker.
asset(Number(1141552511415525, -19)));
2070 STAmount
const prepaymentPenalty{broker.
asset, state.principalOutstanding * Number(36, -3)};
2071 BEAST_EXPECT(prepaymentPenalty == broker.
asset(36));
2074 principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee, state.loanScale);
2081 BEAST_EXPECT(payoffAmount > state.paymentRemaining * (state.periodicPayment + broker.
asset(2).value()));
2083 singlePayment(loanKeylet, verifyLoanStatus, state, payoffAmount, 1, baseFlag,
tfLoanFullPayment);
2088 return [&, baseFlag](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
2092 auto state =
getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2096 STAmount(broker.
asset, state.periodicPayment) == broker.
asset(Number(8333457002039338267, -17)));
2100 auto const startingPayments = state.paymentRemaining;
2101 STAmount
const payoffAmount = [&]() {
2103 auto const rawPayoff = startingPayments * (state.periodicPayment + broker.
asset(2).value());
2104 STAmount payoffAmount{broker.
asset, rawPayoff};
2105 BEAST_EXPECTS(payoffAmount == broker.
asset(Number(1024014840244721, -12)),
to_string(payoffAmount));
2106 BEAST_EXPECT(payoffAmount > state.principalOutstanding);
2108 payoffAmount =
roundToScale(payoffAmount, state.loanScale);
2110 return payoffAmount;
2113 auto const totalPayoffValue = state.totalValue + startingPayments * broker.
asset(2).value();
2114 STAmount
const totalPayoffAmount{broker.
asset, totalPayoffValue};
2117 totalPayoffAmount == payoffAmount,
2118 "Payoff amount: " +
to_string(payoffAmount) +
". Total Value: " +
to_string(totalPayoffAmount));
2120 singlePayment(loanKeylet, verifyLoanStatus, state, payoffAmount, state.paymentRemaining, baseFlag, 0);
2130 "Loan overpayment allowed - Impair and Default",
2144 "Loan overpayment prohibited - Impair and Default",
2154 defaultImmediately(0));
2158 "Loan overpayment allowed - Default without Impair",
2172 "Loan overpayment prohibited - Default without Impair",
2182 defaultImmediately(0,
false));
2186 "Loan overpayment prohibited - Pay off immediately",
2200 "Loan overpayment allowed - Pay off immediately",
2214 "Loan overpayment prohibited - Combine all payments",
2224 combineAllPayments(0));
2228 "Loan overpayment allowed - Combine all payments",
2242 "Loan overpayment prohibited - Make payments",
2252 [&](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
2256 auto state =
getCurrentState(env, broker, loanKeylet, verifyLoanStatus);
2257 BEAST_EXPECT(state.flags == 0);
2260 verifyLoanStatus(state);
2262 env.
close(state.startDate + 20s);
2263 auto const loanAge = (env.
now() - state.startDate).
count();
2264 BEAST_EXPECT(loanAge == 30);
2274 Number
const interval = state.paymentInterval;
2275 auto const periodicRate = interval * Number(12, -2) /
secondsInYear;
2276 BEAST_EXPECT(periodicRate == Number(2283105022831050228, -24, Number::normalized{}));
2277 STAmount
const roundedPeriodicPayment{
2280 testcase << currencyLabel <<
" Payment components: "
2281 <<
"Payments remaining, rawInterest, rawPrincipal, "
2282 "rawMFee, trackedValueDelta, trackedPrincipalDelta, "
2283 "trackedInterestDelta, trackedMgmtFeeDelta, special";
2285 auto const serviceFee = broker.
asset(2);
2288 roundedPeriodicPayment ==
2295 STAmount
const totalDue =
2310 state.totalValue, state.principalOutstanding, state.managementFeeOutstanding);
2311 testcase << currencyLabel <<
" Loan starting state: " << state.paymentRemaining <<
", "
2312 << raw.interestDue <<
", " << raw.principalOutstanding <<
", " << raw.managementFeeDue
2313 <<
", " << rounded.valueOutstanding <<
", " << rounded.principalOutstanding <<
", "
2314 << rounded.interestDue <<
", " << rounded.managementFeeDue;
2319 STAmount
const transactionAmount = STAmount{broker.
asset, totalDue} + broker.
asset(10);
2323 transactionAmount ==
2327 auto const initialState = state;
2328 detail::PaymentComponents totalPaid{
2329 .trackedValueDelta = 0, .trackedPrincipalDelta = 0, .trackedManagementFeeDelta = 0};
2330 Number totalInterestPaid = 0;
2336 while (state.paymentRemaining > 0)
2343 state.principalOutstanding,
2344 state.managementFeeOutstanding,
2345 state.periodicPayment,
2347 state.paymentRemaining,
2352 paymentComponents.trackedValueDelta <= roundedPeriodicPayment,
2353 "Delta: " +
to_string(paymentComponents.trackedValueDelta) +
2354 ", periodic payment: " +
to_string(roundedPeriodicPayment));
2357 state.periodicPayment,
2359 state.paymentRemaining - 1,
2361 detail::LoanStateDeltas
const deltas = currentTrueState - nextTrueState;
2363 testcase << currencyLabel <<
" Payment components: " << state.paymentRemaining <<
", "
2364 << deltas.interest <<
", " << deltas.principal <<
", " << deltas.managementFee <<
", "
2365 << paymentComponents.trackedValueDelta <<
", " << paymentComponents.trackedPrincipalDelta
2366 <<
", " << paymentComponents.trackedInterestPart() <<
", "
2367 << paymentComponents.trackedManagementFeeDelta <<
", "
2372 auto const totalDueAmount =
2373 STAmount{broker.
asset, paymentComponents.trackedValueDelta + serviceFee.number()};
2381 Number
const diff = totalDue - totalDueAmount;
2384 (diff > beast::zero &&
2385 ((broker.
asset.
integral() && (
static_cast<Number
>(diff) < 3)) ||
2386 (state.loanScale - diff.exponent() > 13))));
2389 paymentComponents.trackedValueDelta ==
2390 paymentComponents.trackedPrincipalDelta + paymentComponents.trackedInterestPart() +
2391 paymentComponents.trackedManagementFeeDelta);
2394 paymentComponents.trackedValueDelta <= roundedPeriodicPayment);
2397 state.paymentRemaining < 12 ||
2404 paymentComponents.trackedPrincipalDelta >= beast::zero &&
2405 paymentComponents.trackedPrincipalDelta <= state.principalOutstanding);
2408 paymentComponents.trackedPrincipalDelta == state.principalOutstanding);
2411 (state.periodicPayment.exponent() -
2412 (deltas.principal + deltas.interest + deltas.managementFee - state.periodicPayment)
2415 auto const borrowerBalanceBeforePayment = env.
balance(borrower, broker.
asset);
2424 env(
pay(borrower, loanKeylet.key, transactionAmount));
2429 PrettyAmount adjustment = broker.
asset(0);
2432 adjustment = env.
current()->fees().base;
2436 verifyLoanStatus.checkPayment(
2437 state.loanScale, borrower, borrowerBalanceBeforePayment, totalDueAmount, adjustment);
2439 --state.paymentRemaining;
2440 state.previousPaymentDate = state.nextPaymentDate;
2443 state.paymentRemaining = 0;
2444 state.nextPaymentDate = 0;
2448 state.nextPaymentDate += state.paymentInterval;
2450 state.principalOutstanding -= paymentComponents.trackedPrincipalDelta;
2451 state.managementFeeOutstanding -= paymentComponents.trackedManagementFeeDelta;
2452 state.totalValue -= paymentComponents.trackedValueDelta;
2454 verifyLoanStatus(state);
2456 totalPaid.trackedValueDelta += paymentComponents.trackedValueDelta;
2457 totalPaid.trackedPrincipalDelta += paymentComponents.trackedPrincipalDelta;
2458 totalPaid.trackedManagementFeeDelta += paymentComponents.trackedManagementFeeDelta;
2459 totalInterestPaid += paymentComponents.trackedInterestPart();
2460 ++totalPaymentsMade;
2462 currentTrueState = nextTrueState;
2466 BEAST_EXPECT(state.paymentRemaining == 0);
2467 BEAST_EXPECT(state.principalOutstanding == 0);
2470 BEAST_EXPECT(totalPaid.trackedValueDelta == initialState.totalValue);
2471 BEAST_EXPECT(totalPaid.trackedPrincipalDelta == initialState.principalOutstanding);
2472 BEAST_EXPECT(totalPaid.trackedManagementFeeDelta == initialState.managementFeeOutstanding);
2476 totalInterestPaid ==
2477 initialState.totalValue -
2478 (initialState.principalOutstanding + initialState.managementFeeOutstanding));
2479 BEAST_EXPECT(totalPaymentsMade == initialState.paymentRemaining);
2498 if (!BEAST_EXPECT(timed))
2504 auto const start = clock_type::now();
2506 auto const duration = std::chrono::duration_cast<duration_type>(clock_type::now() - start);
2508 log << label <<
" took " << duration.count() <<
"ms" <<
std::endl;
2525 [&](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
2528 using namespace loan;
2530 auto const state =
getCurrentState(env, broker, verifyLoanStatus.keylet);
2531 auto const serviceFee = broker.
asset(2).value();
2533 STAmount
const totalDue{
2538 time(
"single payment", [&]() { env(
pay(borrower, loanKeylet.key, totalDue)); });
2542 auto const numPayments = (state.paymentRemaining - 2);
2543 STAmount
const bigPayment{broker.
asset, totalDue * numPayments};
2544 XRPAmount
const bigFee{baseFee * (numPayments / loanPaymentsPerFeeIncrement + 1)};
2545 time(
"ten payments", [&]() { env(
pay(borrower, loanKeylet.key, bigPayment), fee(bigFee)); });
2548 time(
"final payment", [&]() {
2550 env(
pay(borrower, loanKeylet.key, totalDue + STAmount{broker.asset, 1}));
2557 "Loan overpayment allowed - Explicit overpayment",
2567 [&](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
2571 "Loan overpayment prohibited - Late payment",
2581 [&](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
2585 "Loan overpayment allowed - Late payment",
2595 [&](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
2599 "Loan overpayment allowed - Late payment and overpayment",
2609 [&](Keylet
const& loanKeylet, VerifyLoanStatus
const& verifyLoanStatus) {
throw 0; });
2617 using namespace jtx;
2619 Account const issuer{
"issuer"};
2620 Account const lender{
"lender"};
2621 Account const borrower{
"borrower"};
2626 bool authorizeBorrower =
false;
2627 int initialXRP = 1'000'000;
2630 auto const testCase = [&,
this](
2633 CaseArgs args = {}) {
2635 env.
fund(
XRP(args.initialXRP), issuer, lender, borrower);
2637 if (args.requireAuth)
2651 mptt.authorize({.account = lender});
2652 mptt.authorize({.account = borrower});
2654 if (args.requireAuth)
2656 mptt.authorize({.account = issuer, .holder = lender});
2657 if (args.authorizeBorrower)
2658 mptt.authorize({.account = issuer, .holder = borrower});
2662 env(
pay(issuer, lender, mptAsset(10'000'000)));
2667 env(
trust(lender, iouAsset(10'000'000)));
2668 env(
trust(borrower, iouAsset(10'000'000)));
2670 if (args.requireAuth)
2673 env(
pay(issuer, lender, iouAsset(10'000'000)));
2674 if (args.authorizeBorrower)
2677 env(
pay(issuer, borrower, iouAsset(10'000)));
2682 env(
pay(issuer, lender, iouAsset(10'000'000)));
2683 env(
pay(issuer, borrower, iouAsset(10'000)));
2690 for (
auto const& asset : assets)
2692 brokers.
emplace_back(createVaultAndBroker(env, asset, lender));
2696 (mptTest)(env, brokers[0], mptt);
2698 (iouTest)(env, brokers[1]);
2703 using namespace loan;
2704 Number const principalRequest = broker.
asset(1'000).value();
2706 testcase(
"MPT issuer is borrower, issuer submits");
2707 env(
set(issuer, broker.
brokerID, principalRequest),
2708 counterparty(lender),
2709 sig(sfCounterpartySignature, lender),
2712 testcase(
"MPT issuer is borrower, lender submits");
2713 env(
set(lender, broker.
brokerID, principalRequest),
2714 counterparty(issuer),
2715 sig(sfCounterpartySignature, issuer),
2719 using namespace loan;
2720 Number const principalRequest = broker.
asset(1'000).value();
2722 testcase(
"IOU issuer is borrower, issuer submits");
2723 env(
set(issuer, broker.
brokerID, principalRequest),
2724 counterparty(lender),
2725 sig(sfCounterpartySignature, lender),
2728 testcase(
"IOU issuer is borrower, lender submits");
2729 env(
set(lender, broker.
brokerID, principalRequest),
2730 counterparty(issuer),
2731 sig(sfCounterpartySignature, issuer),
2734 CaseArgs{.requireAuth =
true});
2738 using namespace loan;
2739 Number const principalRequest = broker.
asset(1'000).value();
2741 testcase(
"MPT unauthorized borrower, borrower submits");
2742 env(
set(borrower, broker.
brokerID, principalRequest),
2743 counterparty(lender),
2744 sig(sfCounterpartySignature, lender),
2748 testcase(
"MPT unauthorized borrower, lender submits");
2749 env(
set(lender, broker.
brokerID, principalRequest),
2750 counterparty(borrower),
2751 sig(sfCounterpartySignature, borrower),
2756 using namespace loan;
2757 Number const principalRequest = broker.
asset(1'000).value();
2759 testcase(
"IOU unauthorized borrower, borrower submits");
2760 env(
set(borrower, broker.
brokerID, principalRequest),
2761 counterparty(lender),
2762 sig(sfCounterpartySignature, lender),
2766 testcase(
"IOU unauthorized borrower, lender submits");
2767 env(
set(lender, broker.
brokerID, principalRequest),
2768 counterparty(borrower),
2769 sig(sfCounterpartySignature, borrower),
2773 CaseArgs{.requireAuth =
true});
2784 using namespace loan;
2785 Number const principalRequest = broker.
asset(1'000).value();
2788 "MPT authorized borrower, borrower submits, borrower has "
2794 auto const sleMPT1 = env.
le(mptoken);
2795 BEAST_EXPECT(sleMPT1 ==
nullptr);
2798 env(
noop(borrower),
fee(
XRP(acctReserve * 2 + incReserve * 2)));
2802 env(
set(borrower, broker.
brokerID, principalRequest),
2803 counterparty(lender),
2804 sig(sfCounterpartySignature, lender),
2806 ter{tecINSUFFICIENT_RESERVE});
2810 env(
pay(issuer, borrower,
XRP(incReserve)));
2812 env(
set(borrower, broker.
brokerID, principalRequest),
2813 counterparty(lender),
2814 sig(sfCounterpartySignature, lender),
2818 auto const sleMPT2 = env.
le(mptoken);
2819 BEAST_EXPECT(sleMPT2 !=
nullptr);
2822 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
2827 using namespace loan;
2828 Number const principalRequest = broker.
asset(1'000).value();
2831 "IOU authorized borrower, borrower submits, borrower has "
2837 env(
pay(borrower, issuer, broker.
asset(10'000)));
2840 auto const sleLine1 = env.
le(trustline);
2841 BEAST_EXPECT(sleLine1 ==
nullptr);
2844 env(
noop(borrower),
fee(
XRP(acctReserve * 2 + incReserve * 2)));
2848 env(
set(borrower, broker.
brokerID, principalRequest),
2849 counterparty(lender),
2850 sig(sfCounterpartySignature, lender),
2852 ter{tecNO_LINE_INSUF_RESERVE});
2856 env(
pay(issuer, borrower,
XRP(incReserve)));
2858 env(
set(borrower, broker.
brokerID, principalRequest),
2859 counterparty(lender),
2860 sig(sfCounterpartySignature, lender),
2864 auto const sleLine2 = env.
le(trustline);
2865 BEAST_EXPECT(sleLine2 !=
nullptr);
2867 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
2871 using namespace loan;
2872 Number const principalRequest = broker.
asset(1'000).value();
2875 "MPT authorized borrower, borrower submits, lender has "
2878 auto const sleMPT1 = env.
le(mptoken);
2879 BEAST_EXPECT(sleMPT1 !=
nullptr);
2881 env(
pay(lender, issuer, broker.
asset(sleMPT1->at(sfMPTAmount))));
2887 auto const sleMPT2 = env.
le(mptoken);
2888 BEAST_EXPECT(sleMPT2 ==
nullptr);
2895 env(
set(borrower, broker.
brokerID, principalRequest),
2896 loanOriginationFee(broker.
asset(1).value()),
2897 counterparty(lender),
2898 sig(sfCounterpartySignature, lender),
2900 ter{tecINSUFFICIENT_RESERVE});
2904 env(
pay(issuer, lender,
XRP(incReserve)));
2906 env(
set(borrower, broker.
brokerID, principalRequest),
2907 loanOriginationFee(broker.
asset(1).value()),
2908 counterparty(lender),
2909 sig(sfCounterpartySignature, lender),
2913 auto const sleMPT3 = env.
le(mptoken);
2914 BEAST_EXPECT(sleMPT3 !=
nullptr);
2917 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
2922 using namespace loan;
2923 Number const principalRequest = broker.
asset(1'000).value();
2926 "IOU authorized borrower, borrower submits, lender has no "
2933 auto const sleLine1 = env.
le(trustline);
2934 BEAST_EXPECT(sleLine1 !=
nullptr);
2936 env(
pay(lender, issuer, broker.
asset(
abs(sleLine1->at(sfBalance).value()))));
2938 auto const sleLine2 = env.
le(trustline);
2939 BEAST_EXPECT(sleLine2 ==
nullptr);
2946 env(
set(borrower, broker.
brokerID, principalRequest),
2947 loanOriginationFee(broker.
asset(1).value()),
2948 counterparty(lender),
2949 sig(sfCounterpartySignature, lender),
2951 ter{tecNO_LINE_INSUF_RESERVE});
2955 env(
pay(issuer, lender,
XRP(incReserve)));
2957 env(
set(borrower, broker.
brokerID, principalRequest),
2958 loanOriginationFee(broker.
asset(1).value()),
2959 counterparty(lender),
2960 sig(sfCounterpartySignature, lender),
2964 auto const sleLine3 = env.
le(trustline);
2965 BEAST_EXPECT(sleLine3 !=
nullptr);
2967 CaseArgs{.initialXRP = acctReserve * 2 + incReserve * 8 + 1});
2971 using namespace loan;
2972 Number const principalRequest = broker.
asset(1'000).value();
2974 testcase(
"MPT authorized borrower, unauthorized lender");
2976 auto const sleMPT1 = env.
le(mptoken);
2977 BEAST_EXPECT(sleMPT1 !=
nullptr);
2979 env(
pay(lender, issuer, broker.
asset(sleMPT1->at(sfMPTAmount))));
2985 auto const sleMPT2 = env.
le(mptoken);
2986 BEAST_EXPECT(sleMPT2 ==
nullptr);
2989 env(
set(borrower, broker.
brokerID, principalRequest),
2990 loanOriginationFee(broker.
asset(1).value()),
2991 counterparty(lender),
2992 sig(sfCounterpartySignature, lender),
2998 env(
set(borrower, broker.
brokerID, principalRequest),
2999 counterparty(lender),
3000 sig(sfCounterpartySignature, lender),
3006 auto const sleMPT3 = env.
le(mptoken);
3007 BEAST_EXPECT(sleMPT3 ==
nullptr);
3010 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3014 using namespace loan;
3015 Number const principalRequest = broker.
asset(1'000).value();
3017 testcase(
"MPT authorized borrower, borrower submits");
3018 env(
set(borrower, broker.
brokerID, principalRequest),
3019 counterparty(lender),
3020 sig(sfCounterpartySignature, lender),
3024 using namespace loan;
3025 Number const principalRequest = broker.
asset(1'000).value();
3027 testcase(
"IOU authorized borrower, borrower submits");
3028 env(
set(borrower, broker.
brokerID, principalRequest),
3029 counterparty(lender),
3030 sig(sfCounterpartySignature, lender),
3033 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3037 using namespace loan;
3038 Number const principalRequest = broker.
asset(1'000).value();
3040 testcase(
"MPT authorized borrower, lender submits");
3041 env(
set(lender, broker.
brokerID, principalRequest),
3042 counterparty(borrower),
3043 sig(sfCounterpartySignature, borrower),
3047 using namespace loan;
3048 Number const principalRequest = broker.
asset(1'000).value();
3050 testcase(
"IOU authorized borrower, lender submits");
3051 env(
set(lender, broker.
brokerID, principalRequest),
3052 counterparty(borrower),
3053 sig(sfCounterpartySignature, borrower),
3056 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3060 auto const msigSetup = [&](
Env& env,
Account const& account) {
3068 using namespace loan;
3069 msigSetup(env, lender);
3070 Number const principalRequest = broker.
asset(1'000).value();
3073 "MPT authorized borrower, borrower submits, lender "
3075 env(
set(borrower, broker.
brokerID, principalRequest),
3076 counterparty(lender),
3077 msig(sfCounterpartySignature, alice, bella),
3081 using namespace loan;
3082 msigSetup(env, lender);
3083 Number const principalRequest = broker.
asset(1'000).value();
3086 "IOU authorized borrower, borrower submits, lender "
3088 env(
set(borrower, broker.
brokerID, principalRequest),
3089 counterparty(lender),
3090 msig(sfCounterpartySignature, alice, bella),
3093 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3097 using namespace loan;
3098 msigSetup(env, borrower);
3099 Number const principalRequest = broker.
asset(1'000).value();
3102 "MPT authorized borrower, lender submits, borrower "
3104 env(
set(lender, broker.
brokerID, principalRequest),
3105 counterparty(borrower),
3106 msig(sfCounterpartySignature, alice, bella),
3110 using namespace loan;
3111 msigSetup(env, borrower);
3112 Number const principalRequest = broker.
asset(1'000).value();
3115 "IOU authorized borrower, lender submits, borrower "
3117 env(
set(lender, broker.
brokerID, principalRequest),
3118 counterparty(borrower),
3119 msig(sfCounterpartySignature, alice, bella),
3122 CaseArgs{.requireAuth =
true, .authorizeBorrower =
true});
3126 using namespace loan;
3127 Number const principalRequest = broker.
asset(1'000).value();
3129 auto tx = vault.set({.owner = lender, .
id = broker.
vaultID});
3130 tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit;
3134 testcase(
"Vault at maximum value");
3135 env(
set(issuer, broker.
brokerID, principalRequest),
3136 counterparty(lender),
3138 sig(sfCounterpartySignature, lender),
3147 using namespace loan;
3148 Number const principalRequest = broker.
asset(1'000).value();
3150 auto tx = vault.set({.owner = lender, .
id = broker.
vaultID});
3151 tx[sfAssetsMaximum] = BrokerParameters::defaults().vaultDeposit + broker.
asset(1).number();
3155 testcase(
"Vault maximum value exceeded");
3156 env(
set(issuer, broker.
brokerID, principalRequest),
3157 counterparty(lender),
3159 sig(sfCounterpartySignature, lender),
3162 paymentInterval(3600 * 24),
3581 using namespace jtx;
3585 auto lowerFee = [&]() {
3591 auto const baseFee = env.
current()->fees().base;
3595 std::string const borrowerSeed =
"ssBRAsLpH4778sLNYC4ik1JBJsBVf";
3597 auto const lenderPass =
"lender";
3598 std::string const lenderSeed =
"shPTCZGwTEhJrYT8NbcNkeaa8pzPM";
3601 env.
fund(
XRP(1'000'000), alice, lender, borrower);
3611 testcase(
"RPC AccountSet");
3613 txJson[sfTransactionType] =
"AccountSet";
3614 txJson[sfAccount] = borrower.
human();
3616 auto const signParams = [&]() {
3618 signParams[jss::passphrase] = borrowerPass;
3619 signParams[jss::key_type] =
"ed25519";
3620 signParams[jss::tx_json] = txJson;
3623 auto const jSign = env.
rpc(
"json",
"sign",
to_string(signParams));
3624 BEAST_EXPECT(jSign.isMember(jss::result) && jSign[jss::result].isMember(jss::tx_json));
3625 auto txSignResult = jSign[jss::result][jss::tx_json];
3626 auto txSignBlob = jSign[jss::result][jss::tx_blob].asString();
3627 txSignResult.removeMember(jss::hash);
3629 auto const jtx = env.
jt(txJson,
sig(borrower));
3630 BEAST_EXPECT(txSignResult == jtx.jv);
3633 auto const jSubmit = env.
rpc(
"submit", txSignBlob);
3635 jSubmit.isMember(jss::result) && jSubmit[jss::result].isMember(jss::engine_result) &&
3636 jSubmit[jss::result][jss::engine_result].asString() ==
"tesSUCCESS");
3643 testcase(
"RPC LoanSet - illegal signature_target");
3646 txJson[sfTransactionType] =
"AccountSet";
3647 txJson[sfAccount] = borrower.
human();
3649 auto const borrowerSignParams = [&]() {
3651 params[jss::passphrase] = borrowerPass;
3652 params[jss::key_type] =
"ed25519";
3653 params[jss::signature_target] =
"Destination";
3654 params[jss::tx_json] = txJson;
3657 auto const jSignBorrower = env.
rpc(
"json",
"sign",
to_string(borrowerSignParams));
3659 jSignBorrower.isMember(jss::result) && jSignBorrower[jss::result].isMember(jss::error) &&
3660 jSignBorrower[jss::result][jss::error] ==
"invalidParams" &&
3661 jSignBorrower[jss::result].isMember(jss::error_message) &&
3662 jSignBorrower[jss::result][jss::error_message] ==
"Destination");
3665 testcase(
"RPC LoanSet - sign and submit borrower initiated");
3668 txJson[sfTransactionType] =
"LoanSet";
3669 txJson[sfAccount] = borrower.
human();
3670 txJson[sfCounterparty] = lender.human();
3671 txJson[sfLoanBrokerID] =
3672 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
3675 txJson[sfPrincipalRequested] =
"100000000";
3676 txJson[sfPaymentTotal] = 10000;
3677 txJson[sfPaymentInterval] = 3600;
3678 txJson[sfGracePeriod] = 300;
3679 txJson[sfFlags] = 65536;
3680 txJson[sfFee] =
to_string(24 * baseFee / 10);
3683 auto const borrowerSignParams = [&]() {
3685 params[jss::passphrase] = borrowerPass;
3686 params[jss::key_type] =
"ed25519";
3687 params[jss::tx_json] = txJson;
3690 auto const jSignBorrower = env.
rpc(
"json",
"sign",
to_string(borrowerSignParams));
3692 jSignBorrower.isMember(jss::result) && jSignBorrower[jss::result].isMember(jss::tx_json),
3694 auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json];
3695 auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString();
3701 auto const jSubmitBlob = env.
rpc(
"submit", txBorrowerSignBlob);
3702 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
3703 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
3704 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
3708 jSubmitBlobResult.isMember(jss::engine_result) &&
3709 jSubmitBlobResult[jss::engine_result].asString() ==
"temBAD_SIGNER");
3714 auto const lenderSignParams = [&]() {
3716 params[jss::passphrase] = lenderPass;
3717 params[jss::key_type] =
"ed25519";
3718 params[jss::signature_target] =
"CounterpartySignature";
3719 params[jss::tx_json] = txBorrowerSignResult;
3722 auto const jSignLender = env.
rpc(
"json",
"sign",
to_string(lenderSignParams));
3723 BEAST_EXPECT(jSignLender.isMember(jss::result) && jSignLender[jss::result].isMember(jss::tx_json));
3724 auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json];
3725 auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString();
3729 auto const jSubmitBlob = env.
rpc(
"submit", txLenderSignBlob);
3730 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
3731 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
3732 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
3733 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
3738 jSubmitBlobResult.isMember(jss::engine_result) &&
3739 jSubmitBlobResult[jss::engine_result].asString() ==
"tecNO_ENTRY",
3742 BEAST_EXPECT(!jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error));
3749 auto const jSubmitJson = env.
rpc(
"json",
"submit",
to_string(lenderSignParams));
3750 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
3751 auto const jSubmitJsonResult = jSubmitJson[jss::result];
3752 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
3753 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
3757 jSubmitJsonResult.isMember(jss::engine_result) &&
3758 jSubmitJsonResult[jss::engine_result].asString() ==
"tefPAST_SEQ",
3761 BEAST_EXPECT(!jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error));
3763 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
3767 testcase(
"RPC LoanSet - sign and submit lender initiated");
3770 txJson[sfTransactionType] =
"LoanSet";
3771 txJson[sfAccount] = lender.human();
3772 txJson[sfCounterparty] = borrower.
human();
3773 txJson[sfLoanBrokerID] =
3774 "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FEC"
3777 txJson[sfPrincipalRequested] =
"100000000";
3778 txJson[sfPaymentTotal] = 10000;
3779 txJson[sfPaymentInterval] = 3600;
3780 txJson[sfGracePeriod] = 300;
3781 txJson[sfFlags] = 65536;
3782 txJson[sfFee] =
to_string(24 * baseFee / 10);
3785 auto const lenderSignParams = [&]() {
3787 params[jss::passphrase] = lenderPass;
3788 params[jss::key_type] =
"ed25519";
3789 params[jss::tx_json] = txJson;
3792 auto const jSignLender = env.
rpc(
"json",
"sign",
to_string(lenderSignParams));
3793 BEAST_EXPECT(jSignLender.isMember(jss::result) && jSignLender[jss::result].isMember(jss::tx_json));
3794 auto const txLenderSignResult = jSignLender[jss::result][jss::tx_json];
3795 auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString();
3801 auto const jSubmitBlob = env.
rpc(
"submit", txLenderSignBlob);
3802 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
3803 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
3804 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
3808 jSubmitBlobResult.isMember(jss::engine_result) &&
3809 jSubmitBlobResult[jss::engine_result].asString() ==
"temBAD_SIGNER");
3814 auto const borrowerSignParams = [&]() {
3816 params[jss::passphrase] = borrowerPass;
3817 params[jss::key_type] =
"ed25519";
3818 params[jss::signature_target] =
"CounterpartySignature";
3819 params[jss::tx_json] = txLenderSignResult;
3822 auto const jSignBorrower = env.
rpc(
"json",
"sign",
to_string(borrowerSignParams));
3823 BEAST_EXPECT(jSignBorrower.isMember(jss::result) && jSignBorrower[jss::result].isMember(jss::tx_json));
3824 auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json];
3825 auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString();
3829 auto const jSubmitBlob = env.
rpc(
"submit", txBorrowerSignBlob);
3830 BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
3831 auto const jSubmitBlobResult = jSubmitBlob[jss::result];
3832 BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
3833 auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
3838 jSubmitBlobResult.isMember(jss::engine_result) &&
3839 jSubmitBlobResult[jss::engine_result].asString() ==
"tecNO_ENTRY",
3842 BEAST_EXPECT(!jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error));
3849 auto const jSubmitJson = env.
rpc(
"json",
"submit",
to_string(borrowerSignParams));
3850 BEAST_EXPECT(jSubmitJson.isMember(jss::result));
3851 auto const jSubmitJsonResult = jSubmitJson[jss::result];
3852 BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
3853 auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
3857 jSubmitJsonResult.isMember(jss::engine_result) &&
3858 jSubmitJsonResult[jss::engine_result].asString() ==
"tefPAST_SEQ",
3861 BEAST_EXPECT(!jSubmitJson.isMember(jss::error) && !jSubmitJsonResult.isMember(jss::error));
3863 BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
4699 testcase <<
"Prevent nextPaymentDueDate overflow";
4701 using namespace jtx;
4702 using namespace std::chrono_literals;
4703 using namespace Lending;
4704 Env env(*
this, all);
4706 Account const issuer{
"issuer"};
4707 Account const lender{
"lender"};
4708 Account const borrower{
"borrower"};
4710 env.
fund(
XRP(1'000'000), issuer, lender, borrower);
4714 auto trustLenderTx = env.
json(trust(lender, iouAsset(1'000'000'000)));
4716 auto trustBorrowerTx = env.
json(trust(borrower, iouAsset(1'000'000'000)));
4717 env(trustBorrowerTx);
4718 auto payLenderTx = pay(issuer, lender, iouAsset(100'000'000));
4720 auto payIssuerTx = pay(issuer, borrower, iouAsset(10'000'000));
4725 BrokerInfo broker{createVaultAndBroker(env, iouAsset, lender, brokerParams)};
4727 using namespace loan;
4729 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
4731 using timeType =
decltype(sfNextPaymentDueDate)::type::value_type;
4734 static_assert(maxTime == 4'294'967'295);
4736 auto const baseJson = [&]() {
4737 auto createJson = env.
json(
4741 gracePeriod(LoanSet::defaultGracePeriod),
4745 loanOriginationFee(218),
4753 auto const baseFee = env.
current()->fees().base;
4755 auto parentCloseTime = [&]() {
return env.
current()->parentCloseTime().time_since_epoch().count(); };
4756 auto maxLoanTime = [&]() {
4757 auto const startDate = parentCloseTime();
4759 BEAST_EXPECT(startDate >= 50);
4761 return maxTime - startDate;
4766 auto const interval = maxLoanTime() + 1;
4767 auto const total = 1;
4768 auto createJson = env.
json(baseJson, paymentInterval(interval), paymentTotal(total));
4770 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
4776 auto const interval = 60;
4777 auto const total = maxLoanTime() + 1;
4780 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
4786 auto const interval = maxLoanTime() + 1;
4787 auto const total = 1;
4788 auto const grace = interval;
4792 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
4797 auto const interval = 1'000'000'000;
4798 auto const total = 10;
4801 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
4807 auto const interval = 60;
4808 auto const total = 1'000'000'000;
4811 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
4817 auto const total = 60;
4818 auto const interval = (maxLoanTime() - total) / total;
4819 auto const grace = interval;
4822 env(createJson, sig(sfCounterpartySignature, lender), ter(tecKILLED));
4827 auto const brokerStateBefore = env.
le(keylet::loanbroker(broker.
brokerID));
4828 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4831 auto const grace = 100;
4832 auto const interval = maxLoanTime() - grace;
4833 auto const total = 1;
4836 env(createJson, sig(sfCounterpartySignature, lender), ter(tesSUCCESS));
4840 auto const meta = env.
meta();
4841 if (BEAST_EXPECT(meta))
4843 BEAST_EXPECT(meta->at(sfTransactionResult) == tecKILLED);
4847 auto const loanSle = env.
le(keylet);
4849 BEAST_EXPECT(!loanSle);
4853 auto const brokerStateBefore = env.
le(keylet::loanbroker(broker.
brokerID));
4854 auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
4857 auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10;
4858 auto const grace = 5'000;
4859 auto const interval = maxTime - closeStartDate - grace;
4860 auto const total = 1;
4863 env(createJson, sig(sfCounterpartySignature, lender), ter(tesSUCCESS));
4867 auto const meta = env.
meta();
4868 if (BEAST_EXPECT(meta))
4870 BEAST_EXPECT(meta->at(sfTransactionResult) == tesSUCCESS);
4874 auto const afterState = getCurrentState(env, broker, keylet);
4875 BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace);
4876 BEAST_EXPECT(afterState.previousPaymentDate == 0);
4877 BEAST_EXPECT(afterState.paymentRemaining == 1);
4882 env(
pay(issuer, borrower, iouAsset(Number{1'055'524'81, -2})));
4885 auto const closeStartDate = (parentCloseTime() / 10 + 1) * 10;
4886 auto const grace = 5'000;
4887 auto const maxLoanTime = maxTime - closeStartDate - grace;
4888 auto const total = [&]() {
4889 if (maxLoanTime % 5 == 0)
4891 if (maxLoanTime % 3 == 0)
4893 if (maxLoanTime % 2 == 0)
4897 if (!BEAST_EXPECT(total != 0))
4900 auto const brokerState = env.
le(keylet::loanbroker(broker.
brokerID));
4902 auto const loanSequence = brokerState->at(sfLoanSequence);
4905 auto const interval = maxLoanTime / total;
4908 env(createJson, sig(sfCounterpartySignature, lender), ter(tesSUCCESS));
4912 auto const beforeState = getCurrentState(env, broker, keylet);
4913 BEAST_EXPECT(beforeState.nextPaymentDate == closeStartDate + interval);
4914 BEAST_EXPECT(beforeState.previousPaymentDate == 0);
4915 BEAST_EXPECT(beforeState.paymentRemaining == total);
4916 BEAST_EXPECT(beforeState.periodicPayment > 0);
4920 NumberRoundModeGuard mg{Number::upward};
4921 Number
const payment = beforeState.periodicPayment * (total - 1);
4922 XRPAmount
const payFee{baseFee * ((total - 1) / loanPaymentsPerFeeIncrement + 1)};
4923 STAmount
const paymentAmount =
roundToScale(STAmount{broker.
asset, payment}, beforeState.loanScale);
4924 auto loanPayTx = env.
json(
pay(borrower,
keylet.
key, paymentAmount), fee(payFee));
4925 env(loanPayTx, ter(tesSUCCESS));
4930 auto const afterState = getCurrentState(env, broker, keylet);
4931 BEAST_EXPECT(afterState.paymentRemaining == 1);
4932 BEAST_EXPECT(afterState.nextPaymentDate == maxTime - grace);
4933 BEAST_EXPECT(afterState.previousPaymentDate == maxTime - grace - interval);
5338 testcase(
"PoC: Unsigned-underflow full-pay accrual after early periodic");
5340 using namespace jtx;
5341 using namespace loan;
5342 using namespace std::chrono_literals;
5344 Env env(*
this, all);
5346 Account const lender{
"poc_lender4"};
5347 Account const borrower{
"poc_borrower4"};
5348 env.
fund(
XRP(3'000'000), lender, borrower);
5353 auto const broker = createVaultAndBroker(env, asset, lender, brokerParams);
5357 auto const loanSetFee =
fee(env.
current()->fees().base * 2);
5358 Number const principalRequest = asset(1000).value();
5359 auto const originationFee = asset(0).value();
5360 auto const serviceFee = asset(1).value();
5361 auto const serviceFeePA = asset(1);
5362 auto const lateFee = asset(0).value();
5363 auto const closeFee = asset(0).value();
5368 auto const total = 3u;
5369 auto const interval = 600u;
5370 auto const grace = 60u;
5372 auto createJtx = env.
jt(
5373 set(borrower, broker.
brokerID, principalRequest, 0),
5374 sig(sfCounterpartySignature, lender),
5375 loanOriginationFee(originationFee),
5376 loanServiceFee(serviceFee),
5377 latePaymentFee(lateFee),
5378 closePaymentFee(closeFee),
5380 interestRate(interest),
5381 lateInterestRate(lateInterest),
5382 closeInterestRate(closeInterest),
5383 overpaymentInterestRate(overpaymentInterest),
5384 paymentTotal(total),
5385 paymentInterval(interval),
5389 auto const brokerSle = env.
le(keylet::loanbroker(broker.
brokerID));
5390 BEAST_EXPECT(brokerSle);
5391 auto const loanSequence = brokerSle ? brokerSle->at(sfLoanSequence) : 0;
5392 auto const loanKeylet = keylet::loan(broker.
brokerID, loanSequence);
5398 auto state = getCurrentState(env, broker, loanKeylet);
5400 auto const components = detail::computePaymentComponents(
5404 state.principalOutstanding,
5405 state.managementFeeOutstanding,
5406 state.periodicPayment,
5408 state.paymentRemaining,
5409 brokerParams.managementFeeRate);
5410 STAmount const regularDue{asset, components.trackedValueDelta + serviceFeePA.number()};
5412 env(pay(borrower, loanKeylet.key, regularDue));
5417 auto after = getCurrentState(env, broker, loanKeylet);
5418 auto const loanSle = env.
le(loanKeylet);
5419 BEAST_EXPECT(loanSle);
5420 auto const brokerSle2 = env.
le(keylet::loanbroker(broker.
brokerID));
5421 BEAST_EXPECT(brokerSle2);
5423 auto const closePaymentFee = loanSle ? loanSle->at(sfClosePaymentFee) :
Number{};
5425 auto const managementFeeRate = brokerSle2 ?
TenthBips16{brokerSle2->at(sfManagementFeeRate)} :
TenthBips16{};
5431 detail::loanPrincipalFromPeriodicPayment(
after.periodicPayment, periodicRate2,
after.paymentRemaining),
5433 env.
current()->parentCloseTime(),
5434 after.paymentInterval,
5435 after.previousPaymentDate,
5440 auto const roundedInterest =
roundToAsset(asset.raw(), fullPaymentInterest,
after.loanScale);
5441 Number const roundedFullMgmtFee =
5443 Number const roundedFullInterest = roundedInterest - roundedFullMgmtFee;
5446 auto const nowSecs =
static_cast<std::uint32_t>(env.
current()->parentCloseTime().time_since_epoch().count());
5447 auto const startSecs =
static_cast<std::uint32_t>(
after.startDate.time_since_epoch().count());
5448 auto const lastPaymentDate =
std::max(
after.previousPaymentDate, startSecs);
5450 auto const unsignedDelta =
static_cast<std::uint32_t>(nowSecs - lastPaymentDate);
5451 log <<
"PoC window: prev=" <<
after.previousPaymentDate <<
" start=" << startSecs <<
" now=" << nowSecs
5452 <<
" signedDelta=" << signedDelta <<
" unsignedDelta=" << unsignedDelta <<
std::endl;
5456 auto const prevClamped =
std::min(
after.previousPaymentDate, nowSecs);
5458 detail::loanPrincipalFromPeriodicPayment(
after.periodicPayment, periodicRate2,
after.paymentRemaining),
5460 env.
current()->parentCloseTime(),
5461 after.paymentInterval,
5465 auto const roundedInterestClamped =
roundToAsset(asset.raw(), fullPaymentInterestClamped,
after.loanScale);
5466 Number const roundedFullMgmtFeeClamped =
5468 Number const roundedFullInterestClamped = roundedInterestClamped - roundedFullMgmtFeeClamped;
5471 after.principalOutstanding + roundedFullInterestClamped + roundedFullMgmtFeeClamped + closePaymentFee};
5474 auto const vaultId2 = brokerSle2 ? brokerSle2->at(sfVaultID) :
uint256{};
5475 auto const vaultKey2 = keylet::vault(vaultId2);
5476 auto const vaultBefore = env.
le(vaultKey2);
5477 BEAST_EXPECT(vaultBefore);
5478 Number const assetsTotalBefore = vaultBefore ? vaultBefore->at(sfAssetsTotal) :
Number{};
5481 asset,
after.principalOutstanding + roundedFullInterest + roundedFullMgmtFee + closePaymentFee};
5483 log <<
"PoC payoff: principalOutstanding=" <<
after.principalOutstanding
5484 <<
" roundedFullInterest=" << roundedFullInterest <<
" roundedFullMgmtFee=" << roundedFullMgmtFee
5485 <<
" closeFee=" << closePaymentFee <<
" fullDue=" << to_string(fullDue.getJson()) <<
std::endl;
5486 log <<
"PoC reference (clamped): roundedFullInterestClamped=" << roundedFullInterestClamped
5487 <<
" roundedFullMgmtFeeClamped=" << roundedFullMgmtFeeClamped
5488 <<
" fullDueClamped=" << to_string(fullDueClamped.getJson()) <<
std::endl;
5495 BEAST_EXPECT(unsignedDelta >
after.paymentInterval);
5498 auto const vaultAfter = env.
le(vaultKey2);
5499 BEAST_EXPECT(vaultAfter);
5502 auto const assetsTotalAfter = vaultAfter->at(sfAssetsTotal);
5503 log <<
"PoC NAV: assetsTotalBefore=" << assetsTotalBefore <<
" assetsTotalAfter=" << assetsTotalAfter
5504 <<
" delta=" << (assetsTotalAfter - assetsTotalBefore) <<
std::endl;
5508 BEAST_EXPECT(fullDue == fullDueClamped);
5509 if (fullDue > fullDueClamped)
5510 log <<
"PoC delta: overcharge (fullDue > clamped)" <<
std::endl;
5514 auto const finalLoan = env.
le(loanKeylet);
5515 BEAST_EXPECT(finalLoan);
5518 BEAST_EXPECT(finalLoan->at(sfPaymentRemaining) == 0);
5519 BEAST_EXPECT(finalLoan->at(sfPrincipalOutstanding) == 0);