diff --git a/include/xrpl/protocol/detail/ledger_entries.macro b/include/xrpl/protocol/detail/ledger_entries.macro index 3d0a4e1b8b..4effb35381 100644 --- a/include/xrpl/protocol/detail/ledger_entries.macro +++ b/include/xrpl/protocol/detail/ledger_entries.macro @@ -558,7 +558,11 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({ {sfPreviousPaymentDate, soeREQUIRED}, {sfNextPaymentDueDate, soeREQUIRED}, {sfPaymentRemaining, soeREQUIRED}, +//#if LOANDRAW +// TODO: Remove this when you remove the rest of the LOANDRAW blocks. +// Directives don't work with macro expansion. {sfAssetsAvailable, soeDEFAULT}, +//#endif {sfPrincipalOutstanding, soeREQUIRED}, // Save the original request amount for rounding / scaling of // other computations, particularly for IOUs diff --git a/include/xrpl/protocol/detail/transactions.macro b/include/xrpl/protocol/detail/transactions.macro index 6a40508adb..c78133038b 100644 --- a/include/xrpl/protocol/detail/transactions.macro +++ b/include/xrpl/protocol/detail/transactions.macro @@ -1031,7 +1031,6 @@ TRANSACTION(ttLOAN_SET, 80, LoanSet, {sfCloseInterestRate, soeOPTIONAL}, {sfOverpaymentInterestRate, soeOPTIONAL}, {sfPrincipalRequested, soeREQUIRED}, - {sfStartDate, soeREQUIRED}, {sfPaymentTotal, soeOPTIONAL}, {sfPaymentInterval, soeOPTIONAL}, {sfGracePeriod, soeOPTIONAL}, @@ -1059,6 +1058,7 @@ TRANSACTION(ttLOAN_MANAGE, 82, LoanManage, {sfLoanID, soeREQUIRED}, })) +#if LOANDRAW /** The Borrower uses this transaction to draws funds from the Loan. */ #if TRANSACTION_INCLUDE # include @@ -1070,6 +1070,7 @@ TRANSACTION(ttLOAN_DRAW, 83, LoanDraw, {sfLoanID, soeREQUIRED}, {sfAmount, soeREQUIRED, soeMPTSupported}, })) +#endif /** The Borrower uses this transaction to make a Payment on the Loan. */ #if TRANSACTION_INCLUDE diff --git a/src/test/app/Batch_test.cpp b/src/test/app/Batch_test.cpp index fc5e58635d..94d044dc1e 100644 --- a/src/test/app/Batch_test.cpp +++ b/src/test/app/Batch_test.cpp @@ -2631,10 +2631,7 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), // Not allowed to include the counterparty signature sig(sfCounterpartySignature, borrower), sig(none), @@ -2642,8 +2639,7 @@ class Batch_test : public beast::unit_test::suite seq(none)), lenderSeq + 1), batch::inner( - draw( - lender, + pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2)); @@ -2655,18 +2651,14 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), // Counterparty must be set sig(none), fee(none), seq(none)), lenderSeq + 1), batch::inner( - draw( - lender, + pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2)); @@ -2678,10 +2670,7 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), // Counterparty must sign the outer transaction counterparty(borrower.id()), sig(none), @@ -2689,8 +2678,7 @@ class Batch_test : public beast::unit_test::suite seq(none)), lenderSeq + 1), batch::inner( - draw( - lender, + pay(lender, loanKeylet.key, STAmount{asset, asset(500).value()}), lenderSeq + 2)); @@ -2706,17 +2694,14 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), counterparty(borrower.id()), sig(none), fee(none), seq(none)), lenderSeq + 1), batch::inner( - draw( + pay( // However, this inner transaction will fail, // because the lender is not allowed to draw the // transaction @@ -2741,10 +2726,7 @@ class Batch_test : public beast::unit_test::suite batch::outer(lender, lenderSeq, batchFee, tfAllOrNothing), batch::inner( env.json( - set(lender, - brokerKeylet.key, - asset(1000).value(), - env.now() + 3600s), + set(lender, brokerKeylet.key, asset(1000).value()), counterparty(borrower.id()), sig(none), fee(none), diff --git a/src/test/app/EscrowToken_test.cpp b/src/test/app/EscrowToken_test.cpp index f6831e378e..e930c2f9ac 100644 --- a/src/test/app/EscrowToken_test.cpp +++ b/src/test/app/EscrowToken_test.cpp @@ -3503,7 +3503,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); // bob can finish escrow env(escrow::finish(bob, alice, seq1), @@ -3522,7 +3522,7 @@ struct EscrowToken_test : public beast::unit_test::suite : MPT(20'000); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == escrowedWithFix); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == escrowedWithFix); - BEAST_EXPECT(env.balance(gw, MPT) == outstandingWithFix); + BEAST_EXPECT(env.balance(gw, MPT) == -outstandingWithFix); } // test locked rate: cancel @@ -3567,7 +3567,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice); BEAST_EXPECT(env.balance(bob, MPT) == preBob); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); } @@ -3608,7 +3608,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 125); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 125); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(20'000)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-20'000)); // bob can finish escrow env(escrow::finish(gw, alice, seq1), @@ -3620,7 +3620,7 @@ struct EscrowToken_test : public beast::unit_test::suite BEAST_EXPECT(env.balance(alice, MPT) == preAlice - delta); BEAST_EXPECT(mptEscrowed(env, alice, MPT) == 0); BEAST_EXPECT(issuerMPTEscrowed(env, MPT) == 0); - BEAST_EXPECT(env.balance(gw, MPT) == MPT(19'875)); + BEAST_EXPECT(env.balance(gw, MPT) == MPT(-19'875)); } } diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index f389b66e9a..c2492bc75b 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -34,6 +34,7 @@ #include #include +#include #include #include @@ -63,8 +64,10 @@ class Loan_test : public beast::unit_test::suite static constexpr auto const coverDepositParameter = 1000; static constexpr auto const coverRateMinParameter = percentageToTenthBips(10); + static constexpr auto const coverRateLiquidationParameter = + percentageToTenthBips(25); static constexpr auto const maxCoveredLoanValue = 1000 * 100 / 10; - static constexpr auto const vaultDeposit = 50'000; + static constexpr auto const vaultDeposit = 1'000'000; static constexpr auto const debtMaximumParameter = 25'000; std::string const iouCurrency{"IOU"}; @@ -91,9 +94,8 @@ class Loan_test : public beast::unit_test::suite // counter party signature is optional on LoanSet. Confirm that by // sending transaction without one. - auto setTx = env.jt( - set(alice, keylet.key, Number(10000), env.now() + 720h), - ter(temDISABLED)); + auto setTx = + env.jt(set(alice, keylet.key, Number(10000)), ter(temDISABLED)); env(setTx); // All loan transactions are disabled. @@ -109,8 +111,10 @@ class Loan_test : public beast::unit_test::suite env(del(alice, loanKeylet.key), ter(temDISABLED)); // 3. LoanManage env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); +#if LOANDRAW && 0 // 4. LoanDraw env(draw(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); +#endif // 5. LoanPay env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); }; @@ -136,11 +140,11 @@ class Loan_test : public beast::unit_test::suite NetClock::time_point startDate = {}; std::uint32_t nextPaymentDate = 0; std::uint32_t paymentRemaining = 0; - Number assetsAvailable = 0; - Number const principalRequested; + Number const principalRequested = 0; Number principalOutstanding = 0; std::uint32_t flags = 0; std::uint32_t paymentInterval = 0; + TenthBips32 const interestRate{}; }; struct VerifyLoanStatus @@ -168,7 +172,6 @@ class Loan_test : public beast::unit_test::suite void checkBroker( - Number const& assetsAvailable, Number const& principalRequested, Number const& principalOutstanding, TenthBips32 interestRate, @@ -200,7 +203,7 @@ class Loan_test : public beast::unit_test::suite Number(1, -8)))); env.test.BEAST_EXPECT( env.balance(pseudoAccount, broker.asset).number() == - brokerSle->at(sfCoverAvailable) + assetsAvailable); + brokerSle->at(sfCoverAvailable)); env.test.BEAST_EXPECT( brokerSle->at(sfOwnerCount) == ownerCount); @@ -239,7 +242,6 @@ class Loan_test : public beast::unit_test::suite std::uint32_t ownerCount) const { checkBroker( - state.assetsAvailable, state.principalRequested, state.principalOutstanding, interestRate, @@ -253,7 +255,6 @@ class Loan_test : public beast::unit_test::suite std::uint32_t previousPaymentDate, std::uint32_t nextPaymentDate, std::uint32_t paymentRemaining, - Number const& assetsAvailable, Number const& principalRequested, Number const& principalOutstanding, std::uint32_t flags) const @@ -267,8 +268,9 @@ class Loan_test : public beast::unit_test::suite loan->at(sfNextPaymentDueDate) == nextPaymentDate); env.test.BEAST_EXPECT( loan->at(sfPaymentRemaining) == paymentRemaining); - env.test.BEAST_EXPECT( - loan->at(sfAssetsAvailable) == assetsAvailable); +#if LOANDRAW + env.test.BEAST_EXPECT(loan->at(sfAssetsAvailable) == 0); +#endif env.test.BEAST_EXPECT( loan->at(sfPrincipalRequested) == principalRequested); env.test.BEAST_EXPECT( @@ -281,7 +283,6 @@ class Loan_test : public beast::unit_test::suite auto const interestRate = TenthBips32{loan->at(sfInterestRate)}; auto const paymentInterval = loan->at(sfPaymentInterval); checkBroker( - assetsAvailable, principalRequested, principalOutstanding, interestRate, @@ -331,7 +332,6 @@ class Loan_test : public beast::unit_test::suite state.previousPaymentDate, state.nextPaymentDate, state.paymentRemaining, - state.assetsAvailable, state.principalRequested, state.principalOutstanding, state.flags); @@ -379,7 +379,7 @@ class Loan_test : public beast::unit_test::suite managementFeeRate(TenthBips16(100)), debtMaximum(debtMaximumValue), coverRateMinimum(TenthBips32(coverRateMinParameter)), - coverRateLiquidation(TenthBips32(percentageToTenthBips(25)))); + coverRateLiquidation(TenthBips32(coverRateLiquidationParameter))); env(coverDeposit(lender, keylet.key, coverDepositValue)); @@ -388,6 +388,85 @@ class Loan_test : public beast::unit_test::suite return {asset, keylet.key}; } + LoanState + getCurrentState( + jtx::Env const& env, + BrokerInfo const& broker, + Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) + { + using namespace std::chrono_literals; + using d = NetClock::duration; + using tp = NetClock::time_point; + // Lookup the current loan state + if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) + { + LoanState state{ + .previousPaymentDate = loan->at(sfPreviousPaymentDate), + .startDate = tp{d{loan->at(sfStartDate)}}, + .nextPaymentDate = loan->at(sfNextPaymentDueDate), + .paymentRemaining = loan->at(sfPaymentRemaining), + .principalRequested = loan->at(sfPrincipalRequested), + .principalOutstanding = loan->at(sfPrincipalOutstanding), + .flags = loan->at(sfFlags), + .paymentInterval = loan->at(sfPaymentInterval), + .interestRate = TenthBips32{loan->at(sfInterestRate)}, + }; + BEAST_EXPECT(state.previousPaymentDate == 0); + BEAST_EXPECT( + tp{d{state.nextPaymentDate}} == state.startDate + 600s); + BEAST_EXPECT(state.paymentRemaining == 12); + BEAST_EXPECT( + state.principalOutstanding == broker.asset(1000).value()); + BEAST_EXPECT( + state.principalOutstanding == state.principalRequested); + BEAST_EXPECT(state.paymentInterval == 600); + + verifyLoanStatus(state); + + return state; + } + + return LoanState{}; + } + + bool + canImpairLoan( + jtx::Env const& env, + BrokerInfo const& broker, + LoanState const& state) + { + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + if (auto const vaultSle = + env.le(keylet::vault(brokerSle->at(sfVaultID))); + BEAST_EXPECT(vaultSle)) + { + // log << vaultSle->getJson() << std::endl; + auto const assetsUnavailable = vaultSle->at(sfAssetsTotal) - + vaultSle->at(sfAssetsAvailable); + auto const interestOutstanding = + loanInterestOutstandingMinusFee( + broker.asset, + state.principalRequested, + state.principalOutstanding, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + TenthBips32{brokerSle->at(sfManagementFeeRate)}); + auto const unrealizedLoss = vaultSle->at(sfLossUnrealized) + + state.principalOutstanding + interestOutstanding; + + if (unrealizedLoss > assetsUnavailable) + { + return false; + } + } + } + return true; + } + void lifecycle( std::string const& caseLabel, @@ -429,7 +508,7 @@ class Loan_test : public beast::unit_test::suite // No loans yet verifyLoanStatus.checkBroker( - 0, broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0); + broker.asset(loanAmount).value(), 0, TenthBips32{0}, 1, 0, 0); if (!BEAST_EXPECT(loanSequence != 0)) return; @@ -444,7 +523,6 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest = broker.asset(loanAmount).value(); - auto const startDate = env.now() + 3600s; auto const originationFee = broker.asset(1).value(); auto const serviceFee = broker.asset(2).value(); auto const lateFee = broker.asset(3).value(); @@ -482,9 +560,11 @@ class Loan_test : public beast::unit_test::suite auto const interval = 600; auto const grace = 60; + auto const borrowerStartbalance = env.balance(borrower, broker.asset); + // Use the defined values auto createJtx = env.jt( - set(borrower, broker.brokerID, principalRequest, startDate, flags), + set(borrower, broker.brokerID, principalRequest, flags), sig(sfCounterpartySignature, lender), loanOriginationFee(originationFee), loanServiceFee(serviceFee), @@ -504,12 +584,29 @@ class Loan_test : public beast::unit_test::suite env.close(); + auto const startDate = + env.current()->info().parentCloseTime.time_since_epoch().count(); + if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); BEAST_EXPECT(brokerSle)) { BEAST_EXPECT(brokerSle->at(sfOwnerCount) == 1); } + { + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = 2 * env.current()->fees().base; + } + + BEAST_EXPECT( + env.balance(borrower, broker.asset).value() == + borrowerStartbalance.value() + principalRequest - + originationFee - adjustment.value()); + } + auto const loanFlags = createJtx.stx->isFlag(tfLoanOverpayment) ? lsfLoanOverpayment : LedgerSpecificFlags(0); @@ -534,27 +631,26 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfCloseInterestRate) == closeInterest); BEAST_EXPECT( loan->at(sfOverpaymentInterestRate) == overpaymentInterest); - BEAST_EXPECT( - loan->at(sfStartDate) == startDate.time_since_epoch().count()); + BEAST_EXPECT(loan->at(sfStartDate) == startDate); BEAST_EXPECT(loan->at(sfPaymentInterval) == interval); BEAST_EXPECT(loan->at(sfGracePeriod) == grace); BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0); BEAST_EXPECT( - loan->at(sfNextPaymentDueDate) == - startDate.time_since_epoch().count() + interval); + loan->at(sfNextPaymentDueDate) == startDate + interval); BEAST_EXPECT(loan->at(sfPaymentRemaining) == total); - BEAST_EXPECT( - loan->at(sfAssetsAvailable) == - principalRequest - originationFee); +#if LOANDRAW + BEAST_EXPECT(loan->at(sfAssetsAvailable) == beast::zero); +#endif BEAST_EXPECT(loan->at(sfPrincipalRequested) == principalRequest); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == principalRequest); } + auto state = getCurrentState(env, broker, keylet, verifyLoanStatus); + verifyLoanStatus( 0, - startDate.time_since_epoch().count() + interval, + startDate + interval, total, - principalRequest - originationFee, principalRequest, principalRequest, loanFlags | 0); @@ -586,13 +682,16 @@ class Loan_test : public beast::unit_test::suite // due env(manage(lender, keylet.key, tfLoanDefault), ter(tecTOO_SOON)); - // Impair the loan - env(manage(lender, keylet.key, tfLoanImpair)); + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible + env(manage(lender, keylet.key, tfLoanImpair), + canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED)); // Unimpair the loan - env(manage(lender, keylet.key, tfLoanUnimpair)); + env(manage(lender, keylet.key, tfLoanUnimpair), + canImpair ? ter(tesSUCCESS) : ter(tecNO_PERMISSION)); - auto const nextDueDate = - startDate.time_since_epoch().count() + interval; + auto const nextDueDate = startDate + interval; env.close(); @@ -600,7 +699,6 @@ class Loan_test : public beast::unit_test::suite 0, nextDueDate, total, - principalRequest - originationFee, principalRequest, principalRequest, loanFlags | 0); @@ -613,15 +711,11 @@ class Loan_test : public beast::unit_test::suite env.close(); // Verify the loan is at EOL - auto const assetsAvailable = [&, &keylet = keylet]() { - if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) - { - BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); - BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); - return loan->at(sfAssetsAvailable); - } - return Number(0); - }(); + if (auto loan = env.le(keylet); BEAST_EXPECT(loan)) + { + BEAST_EXPECT(loan->at(sfPaymentRemaining) == 0); + BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == 0); + } auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -637,16 +731,30 @@ class Loan_test : public beast::unit_test::suite env(del(lender, broker.brokerID), ter(tecNO_ENTRY)); // Delete the loan - env(del(lender, keylet.key)); + // Either the borrower or the lender can delete the loan. Alternate + // between who does it across tests. + static unsigned deleteCounter = 0; + auto const deleter = ++deleteCounter % 2 ? lender : borrower; + env(del(deleter, keylet.key)); env.close(); + PrettyAmount adjustment = broker.asset(0); + if (deleter == borrower) + { + // Need to account for fees if the loan is in XRP + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + } + // No loans left verifyLoanStatus.checkBroker( - 0, broker.asset(1000).value(), 0, interest, 1, 0, 0); + broker.asset(1000).value(), 0, interest, 1, 0, 0); BEAST_EXPECT( env.balance(borrower, broker.asset).value() == - borrowerStartingBalance.value() + assetsAvailable); + borrowerStartingBalance.value() - adjustment); BEAST_EXPECT(env.ownerCount(borrower) == borrowerOwnerCount); if (auto const brokerSle = env.le(keylet::loanbroker(broker.brokerID)); @@ -704,7 +812,6 @@ class Loan_test : public beast::unit_test::suite Number const debtMaximumRequest = broker.asset(debtMaximumParameter).value(); - auto const startDate = env.now() + 3600s; auto const loanSetFee = fee(env.current()->fees().base * 2); auto const pseudoAcct = [&]() { @@ -718,24 +825,20 @@ class Loan_test : public beast::unit_test::suite auto badKeylet = keylet::vault(lender.id(), env.seq(lender)); // Try some failure cases // flags are checked first - env(set(evan, - broker.brokerID, - principalRequest, - startDate, - tfLoanSetMask), + env(set(evan, broker.brokerID, principalRequest, tfLoanSetMask), sig(sfCounterpartySignature, lender), loanSetFee, ter(temINVALID_FLAG)); // field length validation // sfData: good length, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), data(std::string(maxDataPayloadLength, 'X')), loanSetFee, ter(tefBAD_AUTH)); // sfData: too long - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), data(std::string(maxDataPayloadLength + 1, 'Y')), loanSetFee, @@ -743,105 +846,105 @@ class Loan_test : public beast::unit_test::suite // field range validation // sfOverpaymentFee: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), overpaymentFee(maxOverpaymentFee), loanSetFee, ter(tefBAD_AUTH)); // sfOverpaymentFee: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), overpaymentFee(maxOverpaymentFee + 1), loanSetFee, ter(temINVALID)); // sfInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), interestRate(maxInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), interestRate(maxInterestRate + 1), loanSetFee, ter(temINVALID)); // sfLateInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), lateInterestRate(maxLateInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfLateInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), lateInterestRate(maxLateInterestRate + 1), loanSetFee, ter(temINVALID)); // sfCloseInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), closeInterestRate(maxCloseInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfCloseInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), closeInterestRate(maxCloseInterestRate + 1), loanSetFee, ter(temINVALID)); // sfOverpaymentInterestRate: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), overpaymentInterestRate(maxOverpaymentInterestRate), loanSetFee, ter(tefBAD_AUTH)); // sfOverpaymentInterestRate: too big - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), overpaymentInterestRate(maxOverpaymentInterestRate + 1), loanSetFee, ter(temINVALID)); // sfPaymentTotal: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), paymentTotal(LoanSet::minPaymentTotal), loanSetFee, ter(tefBAD_AUTH)); // sfPaymentTotal: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), paymentTotal(LoanSet::minPaymentTotal - 1), loanSetFee, ter(temINVALID)); // sfPaymentInterval: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), paymentInterval(LoanSet::minPaymentInterval), loanSetFee, ter(tefBAD_AUTH)); // sfPaymentInterval: too small (there is no max) - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), paymentInterval(LoanSet::minPaymentInterval - 1), loanSetFee, ter(temINVALID)); // sfGracePeriod: good value, bad account - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, borrower), paymentInterval(LoanSet::minPaymentInterval * 2), gracePeriod(LoanSet::minPaymentInterval * 2), loanSetFee, ter(tefBAD_AUTH)); // sfGracePeriod: larger than paymentInterval - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), paymentInterval(LoanSet::minPaymentInterval * 2), gracePeriod(LoanSet::minPaymentInterval * 3), @@ -849,18 +952,18 @@ class Loan_test : public beast::unit_test::suite ter(temINVALID)); // insufficient fee - single sign - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), sig(sfCounterpartySignature, lender), ter(telINSUF_FEE_P)); // insufficient fee - multisign - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), msig(evan, lender), msig(sfCounterpartySignature, evan, borrower), fee(env.current()->fees().base * 5 - 1), ter(telINSUF_FEE_P)); // multisign sufficient fee, but no signers set up - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), counterparty(lender), msig(evan, lender), msig(sfCounterpartySignature, evan, borrower), @@ -868,51 +971,43 @@ class Loan_test : public beast::unit_test::suite ter(tefNOT_MULTI_SIGNING)); // not the broker owner, no counterparty, not signed by broker // owner - env(set(borrower, broker.brokerID, principalRequest, startDate), + env(set(borrower, broker.brokerID, principalRequest), sig(sfCounterpartySignature, evan), loanSetFee, ter(tefBAD_AUTH)); - // bad start date - in the past - env(set(evan, - broker.brokerID, - principalRequest, - env.closed()->info().closeTime - 1s), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecEXPIRED)); // not the broker owner, counterparty is borrower - env(set(evan, broker.brokerID, principalRequest, startDate), + env(set(evan, broker.brokerID, principalRequest), counterparty(borrower), sig(sfCounterpartySignature, borrower), loanSetFee, ter(tecNO_PERMISSION)); // not a LoanBroker object, no counterparty - env(set(lender, badKeylet.key, principalRequest, startDate), + env(set(lender, badKeylet.key, principalRequest), sig(sfCounterpartySignature, evan), loanSetFee, ter(temBAD_SIGNER)); // not a LoanBroker object, counterparty is valid - env(set(lender, badKeylet.key, principalRequest, startDate), + env(set(lender, badKeylet.key, principalRequest), counterparty(borrower), sig(sfCounterpartySignature, borrower), loanSetFee, ter(tecNO_ENTRY)); // borrower doesn't exist - env(set(lender, broker.brokerID, principalRequest, startDate), + env(set(lender, broker.brokerID, principalRequest), counterparty(alice), sig(sfCounterpartySignature, alice), loanSetFee, ter(terNO_ACCOUNT)); // Request more funds than the vault has available - env(set(evan, broker.brokerID, totalVaultRequest + 1, startDate), + env(set(evan, broker.brokerID, totalVaultRequest + 1), sig(sfCounterpartySignature, lender), loanSetFee, ter(tecINSUFFICIENT_FUNDS)); // Request more funds than the broker's first-loss capital can // cover. - env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1, startDate), + env(set(evan, broker.brokerID, maxCoveredLoanRequest + 1), sig(sfCounterpartySignature, lender), loanSetFee, ter(tecINSUFFICIENT_FUNDS)); @@ -988,18 +1083,15 @@ class Loan_test : public beast::unit_test::suite }(); // Try freezing the accounts that can't be frozen - for (auto const& account : {vaultPseudo, evan, pseudoAcct}) + if (freeze) { - if (freeze) + for (auto const& account : {vaultPseudo, evan}) { // Freeze the account freeze(account); // Try to create a loan with a frozen line - env(set(evan, - broker.brokerID, - debtMaximumRequest, - startDate), + env(set(evan, broker.brokerID, debtMaximumRequest), sig(sfCounterpartySignature, lender), loanSetFee, ter(expectedResult)); @@ -1007,18 +1099,15 @@ class Loan_test : public beast::unit_test::suite // Unfreeze the account BEAST_EXPECT(unfreeze); unfreeze(account); - } - // Ensure the line is unfrozen with a request that is fine - // except too it requests more principal than the broker can - // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), - sig(sfCounterpartySignature, lender), - loanSetFee, - ter(tecLIMIT_EXCEEDED)); + // Ensure the line is unfrozen with a request that is fine + // except too it requests more principal than the broker can + // carry + env(set(evan, broker.brokerID, debtMaximumRequest + 1), + sig(sfCounterpartySignature, lender), + loanSetFee, + ter(tecLIMIT_EXCEEDED)); + } } // Deep freeze the borrower, which prevents them from receiving @@ -1027,7 +1116,7 @@ class Loan_test : public beast::unit_test::suite { // Make sure evan has a trust line that so the issuer can // freeze it. (Don't need to do this for the borrower, - // because LoanDraw will create a line to the borrower + // because LoanSet will create a line to the borrower // automatically.) env(trust(evan, issuer[iouCurrency](100'000))); @@ -1036,7 +1125,6 @@ class Loan_test : public beast::unit_test::suite // implies vaultPseudo, evan, - pseudoAcct, // these accounts can't be deep frozen lender}) { @@ -1044,10 +1132,7 @@ class Loan_test : public beast::unit_test::suite deepfreeze(account); // Try to create a loan with a deep frozen line - env(set(evan, - broker.brokerID, - debtMaximumRequest, - startDate), + env(set(evan, broker.brokerID, debtMaximumRequest), sig(sfCounterpartySignature, lender), loanSetFee, ter(expectedResult)); @@ -1059,10 +1144,7 @@ class Loan_test : public beast::unit_test::suite // Ensure the line is unfrozen with a request that is fine // except too it requests more principal than the broker can // carry - env(set(evan, - broker.brokerID, - debtMaximumRequest + 1, - startDate), + env(set(evan, broker.brokerID, debtMaximumRequest + 1), sig(sfCounterpartySignature, lender), loanSetFee, ter(tecLIMIT_EXCEEDED)); @@ -1073,54 +1155,66 @@ class Loan_test : public beast::unit_test::suite // Finally! Create a loan std::string testData; - auto currentState = [&](Keylet const& loanKeylet, - VerifyLoanStatus const& verifyLoanStatus) { - // Lookup the current loan state - if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan)) - { - LoanState state{ - .previousPaymentDate = loan->at(sfPreviousPaymentDate), - .startDate = tp{d{loan->at(sfStartDate)}}, - .nextPaymentDate = loan->at(sfNextPaymentDueDate), - .paymentRemaining = loan->at(sfPaymentRemaining), - .assetsAvailable = loan->at(sfAssetsAvailable), - .principalRequested = loan->at(sfPrincipalRequested), - .principalOutstanding = loan->at(sfPrincipalOutstanding), - .flags = loan->at(sfFlags), - .paymentInterval = loan->at(sfPaymentInterval), - }; - BEAST_EXPECT(state.previousPaymentDate == 0); - BEAST_EXPECT( - tp{d{state.nextPaymentDate}} == state.startDate + 600s); - BEAST_EXPECT(state.paymentRemaining == 12); - BEAST_EXPECT( - state.assetsAvailable == broker.asset(999).value()); - BEAST_EXPECT( - state.principalOutstanding == broker.asset(1000).value()); - BEAST_EXPECT( - state.principalOutstanding == state.principalRequested); - BEAST_EXPECT(state.paymentInterval == 600); - - verifyLoanStatus(state); - - return state; - } - - return LoanState{ - .previousPaymentDate = 0, - .startDate = tp{d{0}}, - .nextPaymentDate = 0, - .paymentRemaining = 0, - .assetsAvailable = 0, - .principalRequested = 0, - .principalOutstanding = 0, - .flags = 0, - .paymentInterval = 0, + auto coverAvailable = + [&env, this](uint256 const& brokerID, Number const& expected) { + if (auto const brokerSle = env.le(keylet::loanbroker(brokerID)); + BEAST_EXPECT(brokerSle)) + { + auto const available = brokerSle->at(sfCoverAvailable); + BEAST_EXPECT(available == expected); + return available; + } + return Number{}; }; + auto getDefaultInfo = [&env, this]( + LoanState const& state, + BrokerInfo const& broker) { + if (auto const brokerSle = + env.le(keylet::loanbroker(broker.brokerID)); + BEAST_EXPECT(brokerSle)) + { + BEAST_EXPECT( + state.principalRequested == state.principalOutstanding); + auto const interestOutstanding = + loanInterestOutstandingMinusFee( + broker.asset, + state.principalRequested, + state.principalOutstanding, + state.interestRate, + state.paymentInterval, + state.paymentRemaining, + TenthBips32{brokerSle->at(sfManagementFeeRate)}); + auto const defaultAmount = roundToAsset( + broker.asset, + std::min( + tenthBipsOfValue( + tenthBipsOfValue( + brokerSle->at(sfDebtTotal), + coverRateMinParameter), + coverRateLiquidationParameter), + state.principalOutstanding + interestOutstanding), + state.principalRequested); + return std::make_pair(defaultAmount, brokerSle->at(sfOwner)); + } + return std::make_pair(Number{}, AccountID{}); + }; + auto replenishCover = [&env, &coverAvailable]( + BrokerInfo const& broker, + AccountID const& brokerAcct, + Number const& startingCoverAvailable, + Number const& amountToBeCovered) { + coverAvailable( + broker.brokerID, startingCoverAvailable - amountToBeCovered); + env(loanBroker::coverDeposit( + brokerAcct, + broker.brokerID, + STAmount{broker.asset, amountToBeCovered})); + coverAvailable(broker.brokerID, startingCoverAvailable); + env.close(); }; - auto defaultBeforeStartDate = [&](std::uint32_t baseFlag, - bool impair = true) { + auto defaultImmediately = [&](std::uint32_t baseFlag, + bool impair = true) { return [&, impair, baseFlag]( Keylet const& loanKeylet, VerifyLoanStatus const& verifyLoanStatus) { @@ -1129,22 +1223,34 @@ class Loan_test : public beast::unit_test::suite // Default the loan // Initialize values with the current state - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == baseFlag); + auto const& broker = verifyLoanStatus.broker; + auto const startingCoverAvailable = coverAvailable( + broker.brokerID, + broker.asset(coverDepositParameter).number()); + if (impair) { - // Impair the loan - env(manage(lender, loanKeylet.key, tfLoanImpair)); - - state.flags |= tfLoanImpair; - state.nextPaymentDate = - env.now().time_since_epoch().count(); - verifyLoanStatus(state); - - // Once the loan is impaired, it can't be impaired again + // Check the vault + bool const canImpair = canImpairLoan(env, broker, state); + // Impair the loan, if possible env(manage(lender, loanKeylet.key, tfLoanImpair), - ter(tecNO_PERMISSION)); + canImpair ? ter(tesSUCCESS) : ter(tecLIMIT_EXCEEDED)); + + if (canImpair) + { + state.flags |= tfLoanImpair; + state.nextPaymentDate = + env.now().time_since_epoch().count(); + + // Once the loan is impaired, it can't be impaired again + env(manage(lender, loanKeylet.key, tfLoanImpair), + ter(tecNO_PERMISSION)); + } + verifyLoanStatus(state); } auto const nextDueDate = tp{d{state.nextPaymentDate}}; @@ -1158,25 +1264,40 @@ class Loan_test : public beast::unit_test::suite // defaulted env.close(nextDueDate + 60s); +#if LOANDRAW && 0 if (impair) { // Impaired loans can't be drawn against env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); } +#endif + + auto const [amountToBeCovered, brokerAcct] = + getDefaultInfo(state, broker); // Default the loan env(manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // The LoanBroker just lost some of it's first-loss capital. + // Replenish it. + replenishCover( + broker, + brokerAcct, + startingCoverAvailable, + amountToBeCovered); state.flags |= tfLoanDefault; state.paymentRemaining = 0; - state.assetsAvailable = 0; state.principalOutstanding = 0; verifyLoanStatus(state); +#if LOANDRAW && 0 // Defaulted loans can't be drawn against, either env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); +#endif // Once a loan is defaulted, it can't be managed env(manage(lender, loanKeylet.key, tfLoanUnimpair), @@ -1192,8 +1313,10 @@ class Loan_test : public beast::unit_test::suite VerifyLoanStatus const& verifyLoanStatus) { // toEndOfLife // - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == baseFlag); +#if LOANDRAW && 0 auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -1218,15 +1341,20 @@ class Loan_test : public beast::unit_test::suite // PrettyAsset scaling. STAmount const drawAmount{broker.asset, state.assetsAvailable}; env(draw(borrower, loanKeylet.key, drawAmount)); +#else + STAmount const drawAmount = + STAmount(broker.asset, state.principalRequested - 1); +#endif env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); - state.assetsAvailable -= drawAmount; verifyLoanStatus(state); +#if LOANDRAW && 0 BEAST_EXPECT( env.balance(borrower, broker.asset) == borrowerStartingBalance + drawAmount - adjustment); +#endif // Send some bogus pay transactions env(pay(borrower, @@ -1271,6 +1399,8 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( periodicRate == Number(2283105022831050, -21, Number::unchecked{})); + STAmount const principalOutstanding{ + broker.asset, state.principalOutstanding}; STAmount const accruedInterest{ broker.asset, state.principalOutstanding * periodicRate * loanAge / @@ -1282,8 +1412,7 @@ class Loan_test : public beast::unit_test::suite broker.asset, state.principalOutstanding * Number(36, -3)}; BEAST_EXPECT(prepaymentPenalty == broker.asset(36)); STAmount const closePaymentFee = broker.asset(4); - auto const payoffAmount = - STAmount{broker.asset, state.principalOutstanding} + + auto const payoffAmount = principalOutstanding + accruedInterest + prepaymentPenalty + closePaymentFee; BEAST_EXPECT( payoffAmount == @@ -1300,7 +1429,7 @@ class Loan_test : public beast::unit_test::suite env.close(); // Need to account for fees if the loan is in XRP - adjustment = broker.asset(0); + PrettyAmount adjustment = broker.asset(0); if (broker.asset.raw().native()) { adjustment = env.current()->fees().base; @@ -1310,9 +1439,22 @@ class Loan_test : public beast::unit_test::suite state.principalOutstanding = 0; verifyLoanStatus(state); - BEAST_EXPECT( - env.balance(borrower, broker.asset) == - borrowerBalanceBeforePayment - payoffAmount - adjustment); + STAmount const balanceChangeAmount{ + broker.asset, + roundToAsset( + broker.asset, + payoffAmount, + borrowerBalanceBeforePayment.number())}; + { + auto const difference = roundToReference( + env.balance(borrower, broker.asset) - + (borrowerBalanceBeforePayment - + balanceChangeAmount - adjustment), + STAmount{ + broker.asset, + borrowerBalanceBeforePayment.value() * 10}); + BEAST_EXPECT(difference == beast::zero); + } // Can't impair or default a paid off loan env(manage(lender, loanKeylet.key, tfLoanImpair), @@ -1322,13 +1464,13 @@ class Loan_test : public beast::unit_test::suite }; }; - // There are a lot of fields that can be set on a loan, but most of - // them only affect the "math" when a payment is made. The only one - // that really affects behavior is the `tfLoanOverpayment` flag. + // There are a lot of fields that can be set on a loan, but most + // of them only affect the "math" when a payment is made. The + // only one that really affects behavior is the + // `tfLoanOverpayment` flag. lifecycle( caseLabel, - "Loan overpayment allowed - Impair and Default before start " - "date", + "Loan overpayment allowed - Impair and Default", env, loanAmount, interestExponent, @@ -1338,12 +1480,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, tfLoanOverpayment, - defaultBeforeStartDate(lsfLoanOverpayment)); + defaultImmediately(lsfLoanOverpayment)); lifecycle( caseLabel, - "Loan overpayment prohibited - Impair and Default before start " - "date", + "Loan overpayment prohibited - Impair and Default", env, loanAmount, interestExponent, @@ -1353,13 +1494,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, 0, - defaultBeforeStartDate(0)); + defaultImmediately(0)); lifecycle( caseLabel, - "Loan overpayment allowed - Default without Impair before " - "start " - "date", + "Loan overpayment allowed - Default without Impair", env, loanAmount, interestExponent, @@ -1369,13 +1508,11 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, tfLoanOverpayment, - defaultBeforeStartDate(lsfLoanOverpayment, false)); + defaultImmediately(lsfLoanOverpayment, false)); lifecycle( caseLabel, - "Loan overpayment prohibited - Default without Impair before " - "start " - "date", + "Loan overpayment prohibited - Default without Impair", env, loanAmount, interestExponent, @@ -1385,7 +1522,7 @@ class Loan_test : public beast::unit_test::suite broker, pseudoAcct, 0, - defaultBeforeStartDate(0, false)); + defaultImmediately(0, false)); lifecycle( caseLabel, @@ -1404,8 +1541,16 @@ class Loan_test : public beast::unit_test::suite // toEndOfLife // // Initialize values with the current state - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == lsfLoanOverpayment); + + auto const& broker = verifyLoanStatus.broker; + auto const startingCoverAvailable = coverAvailable( + broker.brokerID, + broker.asset(coverDepositParameter).number()); + +#if LOANDRAW && 0 auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -1454,25 +1599,42 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT( env.balance(borrower, broker.asset) == borrowerStartingBalance + drawAmount - adjustment); +#endif // move past the due date + grace period (60s) env.close(tp{d{state.nextPaymentDate}} + 60s + 20s); +#if LOANDRAW && 0 // Try to draw env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); +#endif + + auto const [amountToBeCovered, brokerAcct] = + getDefaultInfo(state, broker); // default the loan env(manage(lender, loanKeylet.key, tfLoanDefault)); + env.close(); + + // The LoanBroker just lost some of it's first-loss capital. + // Replenish it. + replenishCover( + broker, + brokerAcct, + startingCoverAvailable, + amountToBeCovered); + state.paymentRemaining = 0; - state.assetsAvailable = 0; state.principalOutstanding = 0; state.flags |= tfLoanDefault; verifyLoanStatus(state); +#if LOANDRAW && 0 // Same error, different check env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); +#endif // Can't make a payment on it either env(pay(borrower, loanKeylet.key, broker.asset(300)), @@ -1526,13 +1688,14 @@ class Loan_test : public beast::unit_test::suite // toEndOfLife // // Draw and make multiple payments - auto state = currentState(loanKeylet, verifyLoanStatus); + auto state = + getCurrentState(env, broker, loanKeylet, verifyLoanStatus); BEAST_EXPECT(state.flags == 0); - // Advance to the start date of the loan - env.close(state.startDate + 5s); + env.close(); verifyLoanStatus(state); +#if LOANDRAW && 0 auto const borrowerStartingBalance = env.balance(borrower, broker.asset); @@ -1548,15 +1711,17 @@ class Loan_test : public beast::unit_test::suite // PrettyAsset scaling. STAmount const drawAmount{broker.asset, state.assetsAvailable}; env(draw(borrower, loanKeylet.key, drawAmount)); +#endif env.close(state.startDate + 20s); auto const loanAge = (env.now() - state.startDate).count(); BEAST_EXPECT(loanAge == 30); - state.assetsAvailable -= drawAmount; +#if LOANDRAW && 0 verifyLoanStatus(state); BEAST_EXPECT( env.balance(borrower, broker.asset) == borrowerStartingBalance + drawAmount - adjustment); +#endif // Periodic payment amount will consist of // 1. principal outstanding (1000) @@ -1580,8 +1745,8 @@ class Loan_test : public beast::unit_test::suite STAmount const principalRequestedAmount{ broker.asset, state.principalRequested}; - // Compute the payment based on the number of payments - // remaining + // Compute the payment based on the number of + // payments remaining auto const rateFactor = power(1 + periodicRate, state.paymentRemaining); Number const rawPeriodicPayment = @@ -1590,8 +1755,8 @@ class Loan_test : public beast::unit_test::suite STAmount const periodicPayment = roundToReference( STAmount{broker.asset, rawPeriodicPayment}, principalRequestedAmount); - // Only check the first payment since the rounding may - // drift as payments are made + // Only check the first payment since the rounding + // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || STAmount(broker.asset, rawPeriodicPayment) == @@ -1600,8 +1765,8 @@ class Loan_test : public beast::unit_test::suite STAmount const totalDue = roundToReference( periodicPayment + broker.asset(2), principalRequestedAmount); - // Only check the first payment since the rounding may - // drift as payments are made + // Only check the first payment since the rounding + // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || totalDue == @@ -1613,8 +1778,8 @@ class Loan_test : public beast::unit_test::suite // taken STAmount const transactionAmount = STAmount{broker.asset, totalDue} + broker.asset(10); - // Only check the first payment since the rounding may - // drift as payments are made + // Only check the first payment since the rounding + // may drift as payments are made BEAST_EXPECT( state.paymentRemaining < 12 || transactionAmount == @@ -1676,7 +1841,7 @@ class Loan_test : public beast::unit_test::suite env.close(); // Need to account for fees if the loan is in XRP - adjustment = broker.asset(0); + PrettyAmount adjustment = broker.asset(0); if (broker.asset.raw().native()) { adjustment = env.current()->fees().base; @@ -1721,8 +1886,8 @@ class Loan_test : public beast::unit_test::suite testcase("Lifecycle"); using namespace jtx; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for an - // MPT. That'll require three corresponding SAVs. + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. Env env(*this, all); Account const issuer{"issuer"}; @@ -1736,21 +1901,21 @@ class Loan_test : public beast::unit_test::suite // Do not fund alice Account const alice{"alice"}; - // Fund the accounts and trust lines with the same amount so that tests - // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(lender, borrower, evan)); + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender, borrower, evan)); env.close(); // Create assets PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; PrettyAsset const iouAsset = issuer[iouCurrency]; - env(trust(lender, iouAsset(1'000'000))); - env(trust(borrower, iouAsset(1'000'000))); - env(trust(evan, iouAsset(1'000'000))); - env(pay(issuer, evan, iouAsset(100'000))); - env(pay(issuer, lender, iouAsset(100'000))); + env(trust(lender, iouAsset(10'000'000))); + env(trust(borrower, iouAsset(10'000'000))); + env(trust(evan, iouAsset(10'000'000))); + env(pay(issuer, evan, iouAsset(1'000'000))); + env(pay(issuer, lender, iouAsset(10'000'000))); // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, iouAsset(1'000))); + env(pay(issuer, borrower, iouAsset(10'000))); env.close(); MPTTester mptt{env, issuer, mptInitNoFund}; @@ -1760,10 +1925,10 @@ class Loan_test : public beast::unit_test::suite mptt.authorize({.account = lender}); mptt.authorize({.account = borrower}); mptt.authorize({.account = evan}); - env(pay(issuer, lender, mptAsset(100'000))); - env(pay(issuer, evan, mptAsset(100'000))); + env(pay(issuer, lender, mptAsset(10'000'000))); + env(pay(issuer, evan, mptAsset(1'000'000))); // Fund the borrower with enough to cover interest and fees - env(pay(issuer, borrower, mptAsset(1'000))); + env(pay(issuer, borrower, mptAsset(10'000))); env.close(); std::array const assets{xrpAsset, mptAsset, iouAsset}; @@ -1823,8 +1988,8 @@ class Loan_test : public beast::unit_test::suite using namespace jtx; using namespace std::chrono_literals; - // Create 3 loan brokers: one for XRP, one for an IOU, and one for an - // MPT. That'll require three corresponding SAVs. + // Create 3 loan brokers: one for XRP, one for an IOU, and one for + // an MPT. That'll require three corresponding SAVs. Env env(*this, all); Account const issuer{"issuer"}; @@ -1832,9 +1997,9 @@ class Loan_test : public beast::unit_test::suite // brokers. Account const lender{"lender"}; - // Fund the accounts and trust lines with the same amount so that tests - // can use the same values regardless of the asset. - env.fund(XRP(100'000), issuer, noripple(lender)); + // Fund the accounts and trust lines with the same amount so that + // tests can use the same values regardless of the asset. + env.fund(XRP(100'000'000), issuer, noripple(lender)); env.close(); // Use an XRP asset for simplicity @@ -1847,15 +2012,13 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; - // The LoanSet json can be created without a counterparty signature, but - // it will not pass preflight + // The LoanSet json can be created without a counterparty signature, + // but it will not pass preflight auto createJson = env.json( set(lender, broker.brokerID, - broker.asset(principalRequest).value(), - startDate), + broker.asset(principalRequest).value()), fee(loanSetFee)); env(createJson, ter(temBAD_SIGNER)); @@ -1893,6 +2056,8 @@ class Loan_test : public beast::unit_test::suite env.close(); + auto const startDate = env.current()->info().parentCloseTime; + // Loan is successfully created { auto const res = env.rpc("account_objects", lender.human()); @@ -1922,7 +2087,6 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(objects.size() == 1); auto const loan = objects[0u]; - BEAST_EXPECT(loan[sfAssetsAvailable] == "1000000000"); BEAST_EXPECT(loan[sfBorrower] == lender.human()); BEAST_EXPECT(loan[sfCloseInterestRate] == 0); BEAST_EXPECT(loan[sfClosePaymentFee] == "0"); @@ -1954,9 +2118,11 @@ class Loan_test : public beast::unit_test::suite env.close(startDate); +#if LOANDRAW && 0 // Draw the loan env(draw(lender, loanKeylet.key, broker.asset(1000))); env.close(); +#endif // Make a payment env(pay(lender, loanKeylet.key, broker.asset(1000))); } @@ -1973,7 +2139,7 @@ class Loan_test : public beast::unit_test::suite Account const lender{"lender"}; Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), lender, borrower); + env.fund(XRP(vaultDeposit * 100), lender, borrower); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; @@ -1984,10 +2150,9 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; auto forgedLoanSet = - set(borrower, broker.brokerID, principalRequest, startDate, 0); + set(borrower, broker.brokerID, principalRequest, 0); Json::Value randomData{Json::objectValue}; randomData[jss::SigningPubKey] = Json::StaticString{"2600"}; @@ -2046,7 +2211,7 @@ class Loan_test : public beast::unit_test::suite Account const issuer{"issuer"}; Account const lender{"lender"}; - env.fund(XRP(100'000), issuer, noripple(lender)); + env.fund(XRP(vaultDeposit * 100), issuer, noripple(lender)); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; @@ -2063,11 +2228,9 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{1, 3}; - auto const startDate = env.now() + 60s; auto createJson = env.json( - set(lender, broker.brokerID, principalRequest, startDate), - fee(loanSetFee)); + set(lender, broker.brokerID, principalRequest), fee(loanSetFee)); Json::Value counterpartyJson{Json::objectValue}; counterpartyJson[sfTxnSignature] = createJson[sfTxnSignature]; @@ -2085,8 +2248,8 @@ class Loan_test : public beast::unit_test::suite void testLoanPayComputePeriodicPaymentValidRateInvariant() { - testcase - << "LoanPay ripple::detail::computePeriodicPayment : valid rate"; + testcase << "LoanPay ripple::detail::computePeriodicPayment : " + "valid rate"; using namespace jtx; using namespace std::chrono_literals; @@ -2096,7 +2259,7 @@ class Loan_test : public beast::unit_test::suite Account const lender{"lender"}; Account const borrower{"borrower"}; - env.fund(XRP(1'000'000), issuer, lender, borrower); + env.fund(XRP(vaultDeposit * 100), issuer, lender, borrower); env.close(); PrettyAsset const xrpAsset{xrpIssue(), 1'000'000}; @@ -2106,13 +2269,12 @@ class Loan_test : public beast::unit_test::suite auto const loanSetFee = fee(env.current()->fees().base * 2); Number const principalRequest{640562, -5}; - auto const startDate = env.now() + 60s; Number const serviceFee{2462611968}; std::uint32_t const numPayments{4294967295}; auto createJson = env.json( - set(borrower, broker.brokerID, principalRequest, startDate), + set(borrower, broker.brokerID, principalRequest), fee(loanSetFee), loanServiceFee(serviceFee), paymentTotal(numPayments), @@ -2136,7 +2298,7 @@ class Loan_test : public beast::unit_test::suite createJson = env.json(createJson, sig(sfCounterpartySignature, lender)); env(createJson, ter(tesSUCCESS)); - env.close(startDate); + env.close(); if (auto const loan = env.le(keylet); BEAST_EXPECT(loan)) { @@ -2144,11 +2306,13 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); - BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal); } - auto loanDrawTx = env.json( - draw(borrower, keylet.key, STAmount{broker.asset, Number{6}})); +#if LOANDRAW && 0 + auto loanDrawTx = + env.json(draw(borrower, keylet.key, STAmount{broker.asset, Number { + 6 + }})); env(loanDrawTx, ter(tesSUCCESS)); env.close(); @@ -2158,8 +2322,8 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPaymentRemaining) == numPayments); BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT(loan->at(sfPrincipalOutstanding) == actualPrincipal); - BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal - 6); } +#endif auto loanPayTx = env.json( pay(borrower, keylet.key, STAmount{broker.asset, serviceFee + 6})); @@ -2173,21 +2337,28 @@ class Loan_test : public beast::unit_test::suite BEAST_EXPECT(loan->at(sfPrincipalRequested) == actualPrincipal); BEAST_EXPECT( loan->at(sfPrincipalOutstanding) == actualPrincipal - 1); - BEAST_EXPECT(loan->at(sfAssetsAvailable) == actualPrincipal - 6); } } void testRPC() { - // This will expand as more test cases are added. Some functionality is - // tested in other test functions. + // This will expand as more test cases are added. Some functionality + // is tested in other test functions. testcase("RPC"); using namespace jtx; Env env(*this, all); + auto lowerFee = [&]() { + // Run the local fee back down. + while (env.app().getFeeTrack().lowerLocalFee()) + ; + }; + + auto const baseFee = env.current()->fees().base; + Account const alice{"alice"}; std::string const borrowerPass = "borrower"; std::string const borrowerSeed = "ssBRAsLpH4778sLNYC4ik1JBJsBVf"; @@ -2229,6 +2400,7 @@ class Loan_test : public beast::unit_test::suite auto const jtx = env.jt(txJson, sig(borrower)); BEAST_EXPECT(txSignResult == jtx.jv); + lowerFee(); auto const jSubmit = env.rpc("submit", txSignBlob); BEAST_EXPECT( jSubmit.isMember(jss::result) && @@ -2236,6 +2408,7 @@ class Loan_test : public beast::unit_test::suite jSubmit[jss::result][jss::engine_result].asString() == "tesSUCCESS"); + lowerFee(); env(jtx.jv, sig(none), seq(none), fee(none), ter(tefPAST_SEQ)); } @@ -2275,12 +2448,11 @@ class Loan_test : public beast::unit_test::suite "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" "5C"; txJson[sfPrincipalRequested] = "100000000"; - txJson[sfStartDate] = 807730340; txJson[sfPaymentTotal] = 10000; txJson[sfPaymentInterval] = 3600; txJson[sfGracePeriod] = 300; txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = "24"; + txJson[sfFee] = to_string(24 * baseFee / 10); // 2. Borrower signs the transaction auto const borrowerSignParams = [&]() { @@ -2292,16 +2464,19 @@ class Loan_test : public beast::unit_test::suite }(); auto const jSignBorrower = env.rpc("json", "sign", to_string(borrowerSignParams)); - BEAST_EXPECT( + BEAST_EXPECTS( jSignBorrower.isMember(jss::result) && - jSignBorrower[jss::result].isMember(jss::tx_json)); + jSignBorrower[jss::result].isMember(jss::tx_json), + to_string(jSignBorrower)); auto const txBorrowerSignResult = jSignBorrower[jss::result][jss::tx_json]; auto const txBorrowerSignBlob = jSignBorrower[jss::result][jss::tx_blob].asString(); - // 2a. Borrower attempts to submit the transaction. It doesn't work + // 2a. Borrower attempts to submit the transaction. It doesn't + // work { + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; @@ -2335,38 +2510,43 @@ class Loan_test : public beast::unit_test::suite jSignLender[jss::result][jss::tx_blob].asString(); // 5. Lender submits the signed transaction blob + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the signatures - // all validated. Of course the transaction won't succeed because no - // Vault or Broker were created. - BEAST_EXPECT( + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "tecNO_ENTRY"); + jSubmitBlobResult[jss::engine_result].asString() == + "tecNO_ENTRY", + to_string(jSubmitBlobResult)); BEAST_EXPECT( !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - // 4-alt. Lender submits the transaction json originally received - // from the Borrower. It gets signed, but is now a duplicate, so - // fails. Borrower could done this instead of steps 4 and 5. + // 4-alt. Lender submits the transaction json originally + // received from the Borrower. It gets signed, but is now a + // duplicate, so fails. Borrower could done this instead of + // steps 4 and 5. + lowerFee(); auto const jSubmitJson = env.rpc("json", "submit", to_string(lenderSignParams)); BEAST_EXPECT(jSubmitJson.isMember(jss::result)); auto const jSubmitJsonResult = jSubmitJson[jss::result]; BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not going - // anywhere - BEAST_EXPECT( + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == - "tefPAST_SEQ"); + jSubmitJsonResult[jss::engine_result].asString() == + "tefPAST_SEQ", + to_string(jSubmitJsonResult)); BEAST_EXPECT( !jSubmitJson.isMember(jss::error) && @@ -2386,12 +2566,11 @@ class Loan_test : public beast::unit_test::suite "FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F" "5C"; txJson[sfPrincipalRequested] = "100000000"; - txJson[sfStartDate] = 807730340; txJson[sfPaymentTotal] = 10000; txJson[sfPaymentInterval] = 3600; txJson[sfGracePeriod] = 300; txJson[sfFlags] = 65536; // tfLoanOverpayment - txJson[sfFee] = "24"; + txJson[sfFee] = to_string(24 * baseFee / 10); // 2. Lender signs the transaction auto const lenderSignParams = [&]() { @@ -2411,8 +2590,10 @@ class Loan_test : public beast::unit_test::suite auto const txLenderSignBlob = jSignLender[jss::result][jss::tx_blob].asString(); - // 2a. Lender attempts to submit the transaction. It doesn't work + // 2a. Lender attempts to submit the transaction. It doesn't + // work { + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; @@ -2446,38 +2627,43 @@ class Loan_test : public beast::unit_test::suite jSignBorrower[jss::result][jss::tx_blob].asString(); // 5. Borrower submits the signed transaction blob + lowerFee(); auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob); BEAST_EXPECT(jSubmitBlob.isMember(jss::result)); auto const jSubmitBlobResult = jSubmitBlob[jss::result]; BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json)); auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json]; - // To get far enough to return tecNO_ENTRY means that the signatures - // all validated. Of course the transaction won't succeed because no - // Vault or Broker were created. - BEAST_EXPECT( + // To get far enough to return tecNO_ENTRY means that the + // signatures all validated. Of course the transaction won't + // succeed because no Vault or Broker were created. + BEAST_EXPECTS( jSubmitBlobResult.isMember(jss::engine_result) && - jSubmitBlobResult[jss::engine_result].asString() == - "tecNO_ENTRY"); + jSubmitBlobResult[jss::engine_result].asString() == + "tecNO_ENTRY", + to_string(jSubmitBlobResult)); BEAST_EXPECT( !jSubmitBlob.isMember(jss::error) && !jSubmitBlobResult.isMember(jss::error)); - // 4-alt. Borrower submits the transaction json originally received - // from the Lender. It gets signed, but is now a duplicate, so - // fails. Lender could done this instead of steps 4 and 5. + // 4-alt. Borrower submits the transaction json originally + // received from the Lender. It gets signed, but is now a + // duplicate, so fails. Lender could done this instead of steps + // 4 and 5. + lowerFee(); auto const jSubmitJson = env.rpc("json", "submit", to_string(borrowerSignParams)); BEAST_EXPECT(jSubmitJson.isMember(jss::result)); auto const jSubmitJsonResult = jSubmitJson[jss::result]; BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json)); auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json]; - // Since the previous tx claimed a fee, this duplicate is not going - // anywhere - BEAST_EXPECT( + // Since the previous tx claimed a fee, this duplicate is not + // going anywhere + BEAST_EXPECTS( jSubmitJsonResult.isMember(jss::engine_result) && - jSubmitJsonResult[jss::engine_result].asString() == - "tefPAST_SEQ"); + jSubmitJsonResult[jss::engine_result].asString() == + "tefPAST_SEQ", + to_string(jSubmitJsonResult)); BEAST_EXPECT( !jSubmitJson.isMember(jss::error) && diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index e563e7af1b..efe7e0c83b 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -793,7 +793,6 @@ Json::Value set(AccountID const& account, uint256 const& loanBrokerID, Number principalRequested, - NetClock::time_point const& startDate, std::uint32_t flags = 0); auto const counterparty = JTxFieldWrapper(sfCounterparty); @@ -835,12 +834,14 @@ manage(AccountID const& account, uint256 const& loanID, std::uint32_t flags); Json::Value del(AccountID const& account, uint256 const& loanID, std::uint32_t flags = 0); +#if loandraw Json::Value draw( AccountID const& account, uint256 const& loanID, STAmount const& amount, std::uint32_t flags = 0); +#endif Json::Value pay(AccountID const& account, diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index 3f3b6f527e..05959dadfb 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -450,7 +450,6 @@ Json::Value set(AccountID const& account, uint256 const& loanBrokerID, Number principalRequested, - NetClock::time_point const& startDate, std::uint32_t flags) { Json::Value jv; @@ -459,7 +458,6 @@ set(AccountID const& account, jv[sfLoanBrokerID] = to_string(loanBrokerID); jv[sfPrincipalRequested] = to_string(principalRequested); jv[sfFlags] = flags; - jv[sfStartDate] = startDate.time_since_epoch().count(); return jv; } @@ -485,6 +483,7 @@ del(AccountID const& account, uint256 const& loanID, std::uint32_t flags) return jv; } +#if LOANDRAW Json::Value draw( AccountID const& account, @@ -500,6 +499,7 @@ draw( jv[sfFlags] = flags; return jv; } +#endif Json::Value pay(AccountID const& account, diff --git a/src/xrpld/app/tx/detail/LoanDelete.cpp b/src/xrpld/app/tx/detail/LoanDelete.cpp index e2ee709563..4e075d8e41 100644 --- a/src/xrpld/app/tx/detail/LoanDelete.cpp +++ b/src/xrpld/app/tx/detail/LoanDelete.cpp @@ -121,9 +121,10 @@ LoanDelete::doApply() auto const vaultSle = view.peek(keylet ::vault(brokerSle->at(sfVaultID))); if (!vaultSle) return tefBAD_LEDGER; // LCOV_EXCL_LINE - auto const vaultAsset = vaultSle->at(sfAsset); +#if LOANDRAW // transfer any remaining funds to the borrower + auto const vaultAsset = vaultSle->at(sfAsset); auto assetsAvailableProxy = loanSle->at(sfAssetsAvailable); if (assetsAvailableProxy != 0) { @@ -136,6 +137,7 @@ LoanDelete::doApply() WaiveTransferFee::Yes)) return ter; } +#endif // Remove LoanID from Directory of the LoanBroker pseudo-account. if (!view.dirRemove( diff --git a/src/xrpld/app/tx/detail/LoanDraw.cpp b/src/xrpld/app/tx/detail/LoanDraw.cpp index 3b99ccf900..79ec742d5d 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.cpp +++ b/src/xrpld/app/tx/detail/LoanDraw.cpp @@ -1,3 +1,4 @@ +#if LOANDRAW //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled @@ -121,7 +122,7 @@ LoanDraw::preclaim(PreclaimContext const& ctx) if (amount.asset() != asset) { - JLOG(ctx.j.warn()) << "Loan amount does not match the Vault asset."; + JLOG(ctx.j.warn()) << "Draw amount does not match the Vault asset."; return tecWRONG_ASSET; } @@ -189,3 +190,4 @@ LoanDraw::doApply() //------------------------------------------------------------------------------ } // namespace ripple +#endif diff --git a/src/xrpld/app/tx/detail/LoanDraw.h b/src/xrpld/app/tx/detail/LoanDraw.h index a4d0a29b79..471744f32f 100644 --- a/src/xrpld/app/tx/detail/LoanDraw.h +++ b/src/xrpld/app/tx/detail/LoanDraw.h @@ -1,3 +1,4 @@ +#if LOANDRAW //------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled @@ -51,3 +52,4 @@ public: } // namespace ripple #endif +#endif diff --git a/src/xrpld/app/tx/detail/LoanManage.cpp b/src/xrpld/app/tx/detail/LoanManage.cpp index 82cb00917c..345457e6f5 100644 --- a/src/xrpld/app/tx/detail/LoanManage.cpp +++ b/src/xrpld/app/tx/detail/LoanManage.cpp @@ -168,10 +168,15 @@ defaultLoan( auto brokerDebtTotalProxy = brokerSle->at(sfDebtTotal); auto const totalDefaultAmount = principalOutstanding + interestOutstanding; +#if LOANDRAW // The default Amount equals the outstanding principal and interest, // excluding any funds unclaimed by the Borrower. auto loanAssetsAvailableProxy = loanSle->at(sfAssetsAvailable); auto const defaultAmount = totalDefaultAmount - loanAssetsAvailableProxy; +#else + // TODO: get rid of this and just use totalDefaultAmount + auto const defaultAmount = totalDefaultAmount; +#endif // Apply the First-Loss Capital to the Default Amount TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; TenthBips32 const coverRateLiquidation{ @@ -185,7 +190,11 @@ defaultLoan( coverRateLiquidation), defaultAmount), originalPrincipalRequested); +#if LOANDRAW auto const returnToVault = defaultCovered + loanAssetsAvailableProxy; +#else + auto const returnToVault = defaultCovered; +#endif auto const vaultDefaultAmount = defaultAmount - defaultCovered; // Update the Vault object: @@ -250,7 +259,9 @@ defaultLoan( // Update the Loan object: loanSle->setFlag(lsfLoanDefault); loanSle->at(sfPaymentRemaining) = 0; +#if LOANDRAW loanAssetsAvailableProxy = 0; +#endif loanSle->at(sfPrincipalOutstanding) = 0; view.update(loanSle); diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 6ed405aa6d..15b7fcb867 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -82,7 +82,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; auto const paymentRemaining = loanSle->at(sfPaymentRemaining); TenthBips32 const lateInterestRate{loanSle->at(sfLateInterestRate)}; - auto const startDate = loanSle->at(sfStartDate); if (loanSle->at(sfBorrower) != account) { @@ -90,12 +89,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) return tecNO_PERMISSION; } - if (!hasExpired(ctx.view, startDate)) - { - JLOG(ctx.j.warn()) << "Loan has not started yet."; - return tecTOO_SOON; - } - if (paymentRemaining == 0 || principalOutstanding == 0) { JLOG(ctx.j.warn()) << "Loan is already paid off."; diff --git a/src/xrpld/app/tx/detail/LoanSet.cpp b/src/xrpld/app/tx/detail/LoanSet.cpp index 5a5054b1b0..3d44b895d4 100644 --- a/src/xrpld/app/tx/detail/LoanSet.cpp +++ b/src/xrpld/app/tx/detail/LoanSet.cpp @@ -187,12 +187,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) { auto const& tx = ctx.tx; - if (auto const startDate(tx[sfStartDate]); hasExpired(ctx.view, startDate)) - { - JLOG(ctx.j.warn()) << "Start date is in the past."; - return tecEXPIRED; - } - auto const account = tx[sfAccount]; auto const brokerID = tx[sfLoanBrokerID]; @@ -223,7 +217,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) return terNO_ACCOUNT; } - auto const brokerPseudo = brokerSle->at(sfAccount); auto const vault = ctx.view.read(keylet::vault(brokerSle->at(sfVaultID))); if (!vault) // Should be impossible @@ -249,16 +242,6 @@ LoanSet::preclaim(PreclaimContext const& ctx) JLOG(ctx.j.warn()) << "Borrower account is frozen."; return ret; } - // TODO: Remove when LoanDraw is combined with LoanSet - // brokerPseudo is eventually going to send funds to the borrower, so it - // can't be frozen now. It is also going to receive funds, so it can't be - // deep frozen, but being frozen is a prerequisite for being deep frozen, so - // checking the one is sufficient. - if (auto const ret = checkFrozen(ctx.view, brokerPseudo, asset)) - { - JLOG(ctx.j.warn()) << "Broker pseudo-account account is frozen."; - return ret; - } // brokerOwner is going to receive funds if there's an origination fee, so // it can't be deep frozen if (auto const ret = checkDeepFrozen(ctx.view, brokerOwner, asset)) @@ -360,16 +343,26 @@ LoanSet::doApply() if (mPriorBalance < view.fees().accountReserve(ownerCount)) return tecINSUFFICIENT_RESERVE; - // Create a holding for the borrower if one does not already exist. - // Account for the origination fee using two payments // // 1. Transfer loanAssetsAvailable (principalRequested - originationFee) - // from vault pseudo-account to LoanBroker pseudo-account. + // from vault pseudo-account to the borrower. + // Create a holding for the borrower if one does not already exist. + if (auto const ter = addEmptyHolding( + view, + borrower, + borrowerSle->at(sfBalance).value().xrp(), + vaultAsset, + j_); + ter && ter != tecDUPLICATE) + // ignore tecDUPLICATE. That means the holding already exists, and + // is fine here + return ter; + if (auto const ter = accountSend( view, vaultPseudo, - brokerPseudo, + borrower, STAmount{vaultAsset, loanAssetsAvailable}, j_, WaiveTransferFee::Yes)) @@ -414,7 +407,7 @@ LoanSet::doApply() paymentInterval, paymentTotal, managementFeeRate); - auto const startDate = tx[sfStartDate]; + auto const startDate = view.info().closeTime.time_since_epoch().count(); auto loanSequence = brokerSle->at(sfLoanSequence); // Create the loan @@ -453,7 +446,6 @@ LoanSet::doApply() loan->at(sfPreviousPaymentDate) = 0; loan->at(sfNextPaymentDueDate) = startDate + paymentInterval; loan->at(sfPaymentRemaining) = paymentTotal; - loan->at(sfAssetsAvailable) = loanAssetsAvailable; view.insert(loan); // Update the balances in the vault