diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index a73ccb9fc5..034968f8e8 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -91,10 +91,8 @@ class Loan_test : public beast::unit_test::suite env(manage(alice, loanKeylet.key, tfLoanImpair), ter(temDISABLED)); // 4. LoanDraw env(draw(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); -#if 0 // 5. LoanPay env(pay(alice, loanKeylet.key, XRP(500)), ter(temDISABLED)); -#endif }; failAll(all - featureMPTokensV1); failAll(all - featureSingleAssetVault - featureLendingProtocol); @@ -1210,9 +1208,132 @@ class Loan_test : public beast::unit_test::suite env(draw(borrower, loanKeylet.key, broker.asset(100)), ter(tecNO_PERMISSION)); + // Can't make a payment on it either + env(pay(borrower, loanKeylet.key, broker.asset(300)), + ter(tecKILLED)); + // Default }); + lifecycle( + "Loan overpayment prohibited - Pay off immediately", + env, + lender, + borrower, + evan, + broker, + pseudoAcct, + 0, + [&](Keylet const& loanKeylet, + VerifyLoanStatus const& verifyLoanStatus) { + // toEndOfLife + // + auto state = currentState(loanKeylet, verifyLoanStatus); + BEAST_EXPECT(state.flags == lsfLoanOverpayment); + auto const borrowerStartingBalance = + env.balance(borrower, broker.asset); + + // Try to make a payment before the loan starts + env(pay(borrower, loanKeylet.key, broker.asset(500)), + ter(tecTOO_SOON)); + + // Advance to the start date of the loan + env.close(state.startDate + 5s); + + verifyLoanStatus(state); + + // Need to account for fees if the loan is in XRP + PrettyAmount adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + // Draw the entire available balance + // Need to create the STAmount directly to avoid PrettyAsset + // scaling. + STAmount const drawAmount{ + broker.asset, state.assetsAvailable}; + env(draw(borrower, loanKeylet.key, drawAmount)); + env.close(state.startDate + 20s); + + state.assetsAvailable -= drawAmount; + verifyLoanStatus(state); + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerStartingBalance + drawAmount - adjustment); + + // Send some bogus pay transactions + env(pay(borrower, + keylet::loan(uint256(0)).key, + broker.asset(10)), + ter(temINVALID)); + env(pay(borrower, loanKeylet.key, broker.asset(-100)), + ter(temBAD_AMOUNT)); + env(pay(borrower, broker.brokerID, broker.asset(100)), + ter(tecNO_ENTRY)); + env(pay(evan, loanKeylet.key, broker.asset(500)), + ter(tecNO_PERMISSION)); + + { + auto const otherAsset = + broker.asset.raw() == assets[0].raw() ? assets[1] + : assets[0]; + env(pay(borrower, loanKeylet.key, otherAsset(100)), + ter(tecWRONG_ASSET)); + } + + // Get the balance after these failed transactions take fees + auto const borrowerBeforePaymentBalance = + env.balance(borrower, broker.asset); + + // Full payoff amount will consist of + // 1. principal outstanding (1000) + // 2. accrued interest (at 12%) + // 3. prepayment penalty (closeInterest at 3.6%) + // 4. close payment fee (4) + // Calculate these values without the helper functions to + // verify they're working correctly + Number const interval = state.paymentInterval; + auto const periodicRate = + interval * (12 / 100) / (365 * 24 * 60 * 60); + // 20 seconds since the loan started (last env.close call) + STAmount const accruedInterest{ + broker.asset, + state.principalOutstanding * periodicRate * 20 / + interval}; + STAmount const prepaymentPenalty{ + broker.asset, + state.principalOutstanding * 36 / 10 / 100}; + STAmount const closePaymentFee = broker.asset(4); + auto const payoffAmount = + STAmount{broker.asset, state.principalOutstanding} + + accruedInterest + prepaymentPenalty + closePaymentFee; + // Try to pay a little extra to show that it's _not_ taken + auto const transactionAmount = + payoffAmount + broker.asset(10); + BEAST_EXPECT(payoffAmount > drawAmount); + env(pay(borrower, loanKeylet.key, transactionAmount)); + + // Need to account for fees if the loan is in XRP + adjustment = broker.asset(0); + if (broker.asset.raw().native()) + { + adjustment = env.current()->fees().base; + } + + state.paymentRemaining = 0; + state.principalOutstanding = 0; + verifyLoanStatus(state); + + BEAST_EXPECT( + env.balance(borrower, broker.asset) == + borrowerBeforePaymentBalance - payoffAmount - + adjustment); + + // TODO: Try to impair a paid off loan + }); + #if 0 lifecycle( "Loan overpayment prohibited - Pay off", diff --git a/src/test/jtx/TestHelpers.h b/src/test/jtx/TestHelpers.h index e5b3f1f2d4..0ff8e404fd 100644 --- a/src/test/jtx/TestHelpers.h +++ b/src/test/jtx/TestHelpers.h @@ -756,6 +756,12 @@ draw( STAmount const& amount, std::uint32_t flags = 0); +Json::Value +pay(AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags = 0); + } // namespace loan } // namespace jtx diff --git a/src/test/jtx/impl/TestHelpers.cpp b/src/test/jtx/impl/TestHelpers.cpp index f2d9210212..500d30fc1c 100644 --- a/src/test/jtx/impl/TestHelpers.cpp +++ b/src/test/jtx/impl/TestHelpers.cpp @@ -529,6 +529,21 @@ draw( return jv; } +Json::Value +pay(AccountID const& account, + uint256 const& loanID, + STAmount const& amount, + std::uint32_t flags) +{ + Json::Value jv; + jv[sfTransactionType] = jss::LoanPay; + jv[sfAccount] = to_string(account); + jv[sfLoanID] = to_string(loanID); + jv[sfAmount] = amount.getJson(); + jv[sfFlags] = flags; + return jv; +} + } // namespace loan } // namespace jtx } // namespace test