diff --git a/src/test/app/Loan_test.cpp b/src/test/app/Loan_test.cpp index 4617050388..9fd0f8541a 100644 --- a/src/test/app/Loan_test.cpp +++ b/src/test/app/Loan_test.cpp @@ -594,7 +594,7 @@ class Loan_test : public beast::unit_test::suite { // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) + if (broker.asset.native()) { adjustment = 2 * env.current()->fees().base; } @@ -757,7 +757,7 @@ class Loan_test : public beast::unit_test::suite if (deleter == borrower) { // Need to account for fees if the loan is in XRP - if (broker.asset.raw().native()) + if (broker.asset.native()) { adjustment = env.current()->fees().base; } @@ -1061,12 +1061,12 @@ class Loan_test : public beast::unit_test::suite TER> { // Freeze / lock the asset std::function empty; - if (broker.asset.raw().native()) + if (broker.asset.native()) { // XRP can't be frozen return std::make_tuple(empty, empty, empty, tesSUCCESS); } - else if (broker.asset.raw().holds()) + else if (broker.asset.holds()) { auto freeze = [&](Account const& holder) { env(trust(issuer, holder[iouCurrency](0), tfSetFreeze)); @@ -1365,15 +1365,24 @@ class Loan_test : public beast::unit_test::suite // taken auto const transactionAmount = payoffAmount + broker.asset(10); + // Send a transaction that tries to pay more than the borrowers's + // balance + env(pay(borrower, + loanKeylet.key, + STAmount{ + broker.asset, + borrowerBalanceBeforePayment.number() * 2}), + ter(tecINSUFFICIENT_FUNDS)); + env(pay(borrower, loanKeylet.key, transactionAmount)); env.close(); // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) + if (broker.asset.native()) { - adjustment = env.current()->fees().base; + adjustment = env.current()->fees().base * 2; } state.paymentRemaining = 0; @@ -1814,7 +1823,7 @@ class Loan_test : public beast::unit_test::suite // Need to account for fees if the loan is in XRP PrettyAmount adjustment = broker.asset(0); - if (broker.asset.raw().native()) + if (broker.asset.native()) { adjustment = env.current()->fees().base; } @@ -2730,7 +2739,6 @@ class Loan_test : public beast::unit_test::suite env(trust(broker, IOU(20'000'000))); env(pay(issuer, broker, IOU(10'000'000))); - env(trust(borrower, IOU(20'000'000))); env.close(); auto const brokerInfo = createVaultAndBroker(env, IOU, broker); @@ -2745,6 +2753,13 @@ class Loan_test : public beast::unit_test::suite paymentInterval(100), fee(XRP(100))); env.close(); + + env(trust(borrower, IOU(20'000'000))); + // The borrower increases their limit and acquires some IOU so they + // can pay interest + env(pay(issuer, borrower, IOU(500))); + env.close(); + if (auto const le = env.le(keylet::loan(keylet.key)); BEAST_EXPECT(le)) { diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index 54e79a2cb7..64d7f8f518 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -22,6 +22,8 @@ #include #include +#include + namespace ripple { bool @@ -203,6 +205,27 @@ LoanPay::preclaim(PreclaimContext const& ctx) << "Vault pseudo-account can not receive funds (deep frozen)."; return ret; } + // Make sure the borrower has enough funds to make the payment! + // Do not support "partial payments" - if the transaction says to pay X, + // then the account must have X available, even if the loan payment takes + // less. + // Also assume that anybody taking loans is not using "community credit", + // which would let an IOU balance go negative up to the other side's limit. + // This may change in a later version. + if (auto const balance = accountHolds( + ctx.view, + account, + asset, + fhZERO_IF_FROZEN, + ahZERO_IF_UNAUTHORIZED, + ctx.j); + balance < amount) + { + JLOG(ctx.j.warn()) << "Payment amount too large. Amount: " + << to_string(amount.getJson()) + << ". Balance: " << to_string(balance.getJson()); + return tecINSUFFICIENT_FUNDS; + } return tesSUCCESS; } @@ -480,20 +503,36 @@ LoanPay::doApply() : accountHolds( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); - /* - auto const balanceScale = std::max( - {accountBalanceBefore.exponent(), - vaultBalanceBefore.exponent(), - brokerBalanceBefore.exponent(), - accountBalanceAfter.exponent(), - vaultBalanceAfter.exponent(), - brokerBalanceAfter.exponent()}); - */ XRPL_ASSERT_PARTS( accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore == accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, "ripple::LoanPay::doApply", "funds are conserved (with rounding)"); + XRPL_ASSERT_PARTS( + accountBalanceAfter >= beast::zero, + "ripple::LoanPay::doApply", + "positive account balance"); + XRPL_ASSERT_PARTS( + accountBalanceAfter < accountBalanceBefore, + "ripple::LoanPay::doApply", + "account balance decreased"); + XRPL_ASSERT_PARTS( + vaultBalanceAfter >= beast::zero && brokerBalanceAfter >= beast::zero, + "ripple::LoanPay::doApply", + "positive vault and broker balances"); + XRPL_ASSERT_PARTS( + vaultBalanceAfter >= vaultBalanceBefore, + "ripple::LoanPay::doApply", + "vault balance did not decrease"); + XRPL_ASSERT_PARTS( + brokerBalanceAfter >= brokerBalanceBefore, + "ripple::LoanPay::doApply", + "broker balance did not decrease"); + XRPL_ASSERT_PARTS( + vaultBalanceAfter > vaultBalanceBefore || + brokerBalanceAfter > brokerBalanceBefore, + "ripple::LoanPay::doApply", + "vault and/or broker balance increased"); #endif return tesSUCCESS;