diff --git a/src/xrpld/app/misc/LendingHelpers.h b/src/xrpld/app/misc/LendingHelpers.h index c1a18106e4..c1fe42d46e 100644 --- a/src/xrpld/app/misc/LendingHelpers.h +++ b/src/xrpld/app/misc/LendingHelpers.h @@ -37,6 +37,7 @@ struct PaymentParts Number rawPrincipal; Number roundedInterest; Number roundedPrincipal; + // We may not need roundedPayment Number roundedPayment; bool final = false; }; @@ -66,8 +67,7 @@ loanLatePaymentInterest( Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate); + std::uint32_t nextPaymentDueDate); Number loanAccruedInterest( @@ -139,8 +139,6 @@ computePaymentParts( * The principal and interest portions can be derived as follows: * interest = principalOutstanding * periodicRate * principal = periodicPayment - interest - * - * Because those values deal with funds, they need to be rounded. */ Number const rawInterest = referencePrincipal * periodicRate; Number const rawPrincipal = periodicPayment - rawInterest; @@ -153,11 +151,45 @@ computePaymentParts( "ripple::detail::computePaymentParts", "valid raw principal"); - Number const roundedInterest = - roundToAsset(asset, rawInterest, scale, Number::upward); - Number const roundedPrincipal = roundedPeriodicPayment - roundedInterest; + // if (count($A20), MIN(Z19, Z19 - FLOOR(AA19 - Y20, 1)), "") + // Z19 = outstanding principal + // AA19 = reference principal + // Y20 = raw principal + + Number const roundedPrincipal = [&]() { + Number const p = std::max( + Number{}, + std::min( + principalOutstanding, + principalOutstanding - + roundToAsset( + asset, + referencePrincipal - rawPrincipal, + scale, + Number::downward))); + // if the estimated principal payment would leave the principal higher + // than the "total "after payment" value of the loan, make the principal + // payment also take the principal down to that same "after" value. + // This should mean that all interest is paid, or that the loan has some + // tricky parameters. + if (principalOutstanding - p > + totalValueOutstanding - roundedPeriodicPayment) + return roundedPeriodicPayment; + // Use the amount that will get principal outstanding as close to + // reference principal as possible. + return p; + }(); + + // if(count($A20), if(AB19 < $B$5, AB19 - Z19, CEILING($B$10-W20, 1)), "") + // AB19 = total loan value + // $B$5 = periodic payment (unrounded) + // Z19 = outstanding principal + // $B$10 = periodic payment (rounded up) + // W20 = rounded principal + + Number const roundedInterest = roundedPeriodicPayment - roundedPrincipal; XRPL_ASSERT_PARTS( - roundedInterest >= 0, + roundedInterest >= 0 && isRounded(asset, roundedInterest, scale), "ripple::detail::computePaymentParts", "valid rounded interest"); XRPL_ASSERT_PARTS( @@ -213,6 +245,10 @@ computeLoanProperties( { auto const periodicRate = detail::loanPeriodicRate(interestRate, paymentInterval); + XRPL_ASSERT( + interestRate == 0 || periodicRate > 0, + "ripple::loanMakePayment : valid rate"); + auto const periodicPayment = detail::loanPeriodicPayment( principalOutstanding, periodicRate, paymentsRemaining); Number const totalValueOutstanding = [&]() { @@ -445,9 +481,8 @@ loanLatePaymentInterest( Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate, - Number const& scale) + std::uint32_t nextPaymentDueDate, + std::int32_t const& scale) { return roundToAsset( asset, @@ -455,8 +490,7 @@ loanLatePaymentInterest( principalOutstanding, lateInterestRate, parentCloseTime, - startDate, - prevPaymentDate), + nextPaymentDueDate), scale); } @@ -484,9 +518,34 @@ struct LoanPaymentParts */ Number valueChange; /// fee_paid is the amount of fee that the payment covered. - Number feePaid; + Number feeToPay; }; +template +void +doPayment( + PaymentParts const& payment, + NumberProxy& totalValueOutstandingProxy, + NumberProxy& principalOutstandingProxy, + NumberProxy& referencePrincipalProxy, + Int32Proxy& paymentRemainingProxy, + Int32Proxy& prevPaymentDateProxy, + Int32Proxy& nextDueDateProxy, + std::uint32_t paymentInterval) +{ + paymentRemainingProxy -= 1; + // A single payment always pays the same amount of principal. Only the + // interest and fees are extra for a late payment + referencePrincipalProxy -= payment.rawPrincipal; + principalOutstandingProxy -= payment.roundedPrincipal; + totalValueOutstandingProxy -= + payment.roundedPrincipal + payment.roundedInterest; + + // Make sure this does an assignment + prevPaymentDateProxy = nextDueDateProxy; + nextDueDateProxy += paymentInterval; +} + /* Handle possible late payments. * * If this function processed a late payment, the return value will be @@ -501,25 +560,20 @@ struct LoanPaymentParts * * section 3.2.4.1.2 (Late Payment) */ template -Expected +Expected, TER> handleLatePayment( A const& asset, ApplyView& view, NumberProxy& principalOutstandingProxy, - Int32Proxy& paymentRemainingProxy, - Int32Proxy& prevPaymentDateProxy, Int32Proxy& nextDueDateProxy, PaymentParts const& periodic, - std::uint32_t const startDate, - std::uint32_t const paymentInterval, TenthBips32 const lateInterestRate, std::int32_t loanScale, + Number const& paymentFee, Number const& latePaymentFee, STAmount const& amount, beast::Journal j) { - return Unexpected(temDISABLED); -#if LOANCOMPLETE if (!hasExpired(view, nextDueDateProxy)) return Unexpected(tesSUCCESS); @@ -531,17 +585,23 @@ handleLatePayment( principalOutstandingProxy, lateInterestRate, view.parentCloseTime(), - startDate, - prevPaymentDateProxy, + nextDueDateProxy, loanScale); XRPL_ASSERT( latePaymentInterest >= 0, "ripple::handleLatePayment : valid late interest"); PaymentParts const late{ - .interest = latePaymentInterest + periodic.interest, - .principal = periodic.principal, - .fee = latePaymentFee + periodic.fee}; - auto const totalDue = late.principal + late.interest + late.fee; + .rawInterest = periodic.rawInterest + latePaymentInterest, + .rawPrincipal = periodic.rawPrincipal, + .roundedInterest = periodic.roundedInterest + latePaymentInterest, + .roundedPrincipal = periodic.roundedPrincipal, + .roundedPayment = periodic.roundedPayment}; + auto const fee = paymentFee + latePaymentFee; + auto const totalDue = late.roundedPrincipal + late.roundedInterest + fee; + XRPL_ASSERT_PARTS( + isRounded(asset, totalDue, loanScale), + "ripple::handleLatePayment", + "total due is rounded"); if (amount < totalDue) { @@ -550,23 +610,15 @@ handleLatePayment( return Unexpected(tecINSUFFICIENT_PAYMENT); } - paymentRemainingProxy -= 1; - // A single payment always pays the same amount of principal. Only the - // interest and fees are extra for a late payment - principalOutstandingProxy -= late.principal; - - // Make sure this does an assignment - prevPaymentDateProxy = nextDueDateProxy; - nextDueDateProxy += paymentInterval; - // A late payment increases the value of the loan by the difference // between periodic and late payment interest - return LoanPaymentParts{ - .principalPaid = late.principal, - .interestPaid = late.interest, - .valueChange = latePaymentInterest, - .feePaid = late.fee}; -#endif + return std::make_pair( + late, + LoanPaymentParts{ + .principalPaid = late.roundedPrincipal, + .interestPaid = late.roundedInterest, + .valueChange = latePaymentInterest, + .feeToPay = fee}); } /* Handle possible full payments. @@ -641,7 +693,7 @@ handleFullPayment( .principalPaid = principalOutstandingProxy, .interestPaid = totalInterest, .valueChange = valueChange, - .feePaid = closePaymentFee}; + .feeToPay = closePaymentFee}; paymentRemainingProxy = 0; principalOutstandingProxy = 0; @@ -664,7 +716,9 @@ loanMakePayment( */ std::int32_t const loanScale = loan->at(sfLoanScale); auto totalValueOutstandingProxy = loan->at(sfTotalValueOutstanding); + auto interestOwedProxy = loan->at(sfInterestOwed); auto principalOutstandingProxy = loan->at(sfPrincipalOutstanding); + auto referencePrincipalProxy = loan->at(sfReferencePrincipal); bool const allowOverpayment = loan->isFlag(lsfLoanOverpayment); TenthBips32 const interestRate{loan->at(sfInterestRate)}; @@ -679,7 +733,7 @@ loanMakePayment( roundToAsset(asset, loan->at(sfClosePaymentFee), loanScale); TenthBips32 const overpaymentFee{loan->at(sfOverpaymentFee)}; - std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); + auto const periodicPayment = loan->at(sfPeriodicPayment); auto paymentRemainingProxy = loan->at(sfPaymentRemaining); auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate); @@ -693,6 +747,7 @@ loanMakePayment( return Unexpected(tecKILLED); } + std::uint32_t const paymentInterval = loan->at(sfPaymentInterval); // Compute the normal periodic rate, payment, etc. // We'll need it in the remaining calculations Number const periodicRate = @@ -701,66 +756,61 @@ loanMakePayment( interestRate == 0 || periodicRate > 0, "ripple::loanMakePayment : valid rate"); - // Don't round the payment amount. Only round the final computations - // using it. - Number const periodicPaymentAmount = detail::loanPeriodicPayment( - principalOutstandingProxy, periodicRate, paymentRemainingProxy); XRPL_ASSERT( - periodicPaymentAmount > 0, - "ripple::computePeriodicPayment : valid payment"); + *totalValueOutstandingProxy > 0, + "ripple::loanMakePayment : valid total value"); + XRPL_ASSERT_PARTS( + *interestOwedProxy >= 0, + "ripple::loanMakePayment", + "valid interest owed"); - auto const periodic = computePaymentParts( + view.update(loan); + + auto const periodic = detail::computePaymentParts( asset, loanScale, totalValueOutstandingProxy, principalOutstandingProxy, - periodicPaymentAmount, - serviceFee, + referencePrincipalProxy, + periodicPayment, periodicRate, paymentRemainingProxy); - Number const totalValueOutstanding = loanTotalValueOutstanding( - asset, loanScale, periodicPaymentAmount, paymentRemainingProxy); - XRPL_ASSERT( - totalValueOutstanding > 0, - "ripple::loanMakePayment : valid total value"); - Number const totalInterestOutstanding = loanTotalInterestOutstanding( - principalOutstandingProxy, totalValueOutstanding); - XRPL_ASSERT_PARTS( - totalInterestOutstanding >= 0, - "ripple::loanMakePayment", - "valid total interest"); - XRPL_ASSERT_PARTS( - totalValueOutstanding - totalInterestOutstanding == - principalOutstandingProxy, - "ripple::loanMakePayment", - "valid principal computation"); - - view.update(loan); - // ------------------------------------------------------------- // late payment handling if (auto const latePaymentParts = handleLatePayment( asset, view, principalOutstandingProxy, - paymentRemainingProxy, - prevPaymentDateProxy, nextDueDateProxy, periodic, - startDate, - paymentInterval, lateInterestRate, loanScale, + serviceFee, latePaymentFee, amount, j)) - return *latePaymentParts; + { + doPayment( + latePaymentParts->first, + totalValueOutstandingProxy, + principalOutstandingProxy, + referencePrincipalProxy, + paymentRemainingProxy, + prevPaymentDateProxy, + nextDueDateProxy, + paymentInterval); + + return latePaymentParts->second; + } else if (latePaymentParts.error()) - return latePaymentParts; + return Unexpected(latePaymentParts.error()); // ------------------------------------------------------------- // full payment handling + auto const totalInterestOutstanding = + totalValueOutstandingProxy - principalOutstandingProxy; + if (auto const fullPaymentParts = handleFullPayment( asset, view, @@ -783,11 +833,16 @@ loanMakePayment( // ------------------------------------------------------------- // regular periodic payment handling + return Unexpected(temDISABLED); +#if LOANCOMPLETE // if the payment is not late nor if it's a full payment, then it must // be a periodic one, with possible overpayments auto const totalDue = periodic.interest + periodic.principal + periodic.fee; + // TODO: Don't attempt to figure out the number of payments beforehand. Just + // loop over making payments until the `amount` is used up or the loan is + // paid off. std::optional mg(Number::downward); std::int64_t const fullPeriodicPayments = [&]() { std::int64_t const full{amount / totalDue}; @@ -826,7 +881,7 @@ loanMakePayment( loanScale, totalValueOutstandingProxy, principalOutstandingProxy, - periodicPaymentAmount, + periodicPayment, serviceFee, periodicRate, paymentRemainingProxy); @@ -845,7 +900,7 @@ loanMakePayment( future.reset(); } - Number totalFeePaid = serviceFee * fullPeriodicPayments; + Number totalfeeToPay = serviceFee * fullPeriodicPayments; Number const newInterest = loanTotalInterestOutstanding( asset, @@ -863,7 +918,7 @@ loanMakePayment( { Number const overpayment = std::min( principalOutstandingProxy.value(), - amount - (totalPrincipalPaid + totalInterestPaid + totalFeePaid)); + amount - (totalPrincipalPaid + totalInterestPaid + totalfeeToPay)); if (roundToAsset(asset, overpayment, loanScale) > 0) { @@ -885,7 +940,7 @@ loanMakePayment( overpaymentInterestPortion = interestPortion; totalPrincipalPaid += remainder; totalInterestPaid += interestPortion; - totalFeePaid += feePortion; + totalfeeToPay += feePortion; principalOutstandingProxy -= remainder; } @@ -908,13 +963,14 @@ loanMakePayment( roundToAsset(asset, loanValueChange, loanScale) == loanValueChange, "ripple::loanMakePayment : loanValueChange rounded"); XRPL_ASSERT( - roundToAsset(asset, totalFeePaid, loanScale) == totalFeePaid, - "ripple::loanMakePayment : totalFeePaid rounded"); + roundToAsset(asset, totalfeeToPay, loanScale) == totalfeeToPay, + "ripple::loanMakePayment : totalfeeToPay rounded"); return LoanPaymentParts{ .principalPaid = totalPrincipalPaid, .interestPaid = totalInterestPaid, .valueChange = loanValueChange, - .feePaid = totalFeePaid}; + .feeToPay = totalfeeToPay}; +#endif } } // namespace ripple diff --git a/src/xrpld/app/misc/detail/LendingHelpers.cpp b/src/xrpld/app/misc/detail/LendingHelpers.cpp index c56e05acff..a64ffb92b4 100644 --- a/src/xrpld/app/misc/detail/LendingHelpers.cpp +++ b/src/xrpld/app/misc/detail/LendingHelpers.cpp @@ -94,20 +94,18 @@ loanLatePaymentInterest( Number const& principalOutstanding, TenthBips32 lateInterestRate, NetClock::time_point parentCloseTime, - std::uint32_t startDate, - std::uint32_t prevPaymentDate) + std::uint32_t nextPaymentDueDate) { /* * This formula is from the XLS-66 spec, section 3.2.4.1.2 (Late payment), * specifically "latePaymentInterest = ..." + * + * The spec is to be updated to base the duration on the next due date */ - auto const lastPaymentDate = std::max(prevPaymentDate, startDate); + auto const secondsOverdue = + parentCloseTime.time_since_epoch().count() - nextPaymentDueDate; - auto const secondsSinceLastPayment = - parentCloseTime.time_since_epoch().count() - lastPaymentDate; - - auto const rate = - loanPeriodicRate(lateInterestRate, secondsSinceLastPayment); + auto const rate = loanPeriodicRate(lateInterestRate, secondsOverdue); return principalOutstanding * rate; } diff --git a/src/xrpld/app/tx/detail/LoanPay.cpp b/src/xrpld/app/tx/detail/LoanPay.cpp index a74810b8e2..48eaab83a3 100644 --- a/src/xrpld/app/tx/detail/LoanPay.cpp +++ b/src/xrpld/app/tx/detail/LoanPay.cpp @@ -122,8 +122,6 @@ LoanPay::preclaim(PreclaimContext const& ctx) TER LoanPay::doApply() { - return temDISABLED; -#if LOANCOMPLETE auto const& tx = ctx_.tx; auto& view = ctx_.view(); @@ -149,37 +147,13 @@ LoanPay::doApply() //------------------------------------------------------ // Loan object state changes - std::int32_t const loanScale = loanSle->at(sfLoanScale); // Unimpair the loan if it was impaired. Do this before the payment is // attempted, so the original values can be used. If the payment fails, this // change will be discarded. if (loanSle->isFlag(lsfLoanImpaired)) { - TenthBips32 const interestRate{loanSle->at(sfInterestRate)}; - auto const principalOutstanding = loanSle->at(sfPrincipalOutstanding); - - TenthBips32 const managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const paymentInterval = loanSle->at(sfPaymentInterval); - auto const paymentsRemaining = loanSle->at(sfPaymentRemaining); - - auto const interestOutstanding = loanInterestOutstandingMinusFee( - asset, - loanScale, - principalOutstanding.value(), - interestRate, - paymentInterval, - paymentsRemaining, - managementFeeRate); - - LoanManage::unimpairLoan( - view, - loanSle, - vaultSle, - principalOutstanding, - interestOutstanding, - paymentInterval, - j_); + LoanManage::unimpairLoan(view, loanSle, vaultSle, j_); } Expected paymentParts = @@ -193,7 +167,8 @@ LoanPay::doApply() view.update(loanSle); XRPL_ASSERT_PARTS( - paymentParts->principalPaid > 0, + // It is possible to pay 0 interest + paymentParts->principalPaid >= 0, "ripple::LoanPay::doApply", "valid principal paid"); XRPL_ASSERT_PARTS( @@ -201,78 +176,83 @@ LoanPay::doApply() "ripple::LoanPay::doApply", "valid interest paid"); XRPL_ASSERT_PARTS( - paymentParts->feePaid >= 0, + paymentParts->feeToPay >= 0, "ripple::LoanPay::doApply", "valid fee paid"); - if (paymentParts->principalPaid <= 0 || paymentParts->interestPaid < 0 || - paymentParts->feePaid < 0) + if (paymentParts->principalPaid < 0 || paymentParts->interestPaid < 0 || + paymentParts->feeToPay < 0) { // LCOV_EXCL_START JLOG(j_.fatal()) << "Loan payment computation returned invalid values."; - return tecINTERNAL; + return tecLIMIT_EXCEEDED; // LCOV_EXCL_STOP } + std::int32_t const loanScale = loanSle->at(sfLoanScale); + //------------------------------------------------------ // LoanBroker object state changes view.update(brokerSle); TenthBips32 managementFeeRate{brokerSle->at(sfManagementFeeRate)}; - auto const managementFee = roundToAsset( - asset, - tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), - loanScale); + auto interestOwedProxy = loanSle->at(sfInterestOwed); - auto const totalPaidToVault = paymentParts->principalPaid + - paymentParts->interestPaid - managementFee; + auto const [managementFee, interestPaidToVault] = [&]() { + auto const managementFee = roundToAsset( + asset, + tenthBipsOfValue(paymentParts->interestPaid, managementFeeRate), + loanScale); + auto const interest = paymentParts->interestPaid - managementFee; + auto const owed = *interestOwedProxy; + if (interest > owed) + return std::make_pair(interest - owed, owed); + return std::make_pair(managementFee, interest); + }(); + XRPL_ASSERT_PARTS( + managementFee >= 0 && interestPaidToVault >= 0 && + (managementFee + interestPaidToVault == + paymentParts->interestPaid) && + isRounded(asset, managementFee, loanScale) && + isRounded(asset, interestPaidToVault, loanScale), + "ripple::LoanPay::doApply", + "management fee computation is valid"); + auto const totalPaidToVault = + paymentParts->principalPaid + interestPaidToVault; - auto const totalPaidToBroker = paymentParts->feePaid + managementFee; + auto const totalPaidToBroker = paymentParts->feeToPay + managementFee; XRPL_ASSERT_PARTS( (totalPaidToVault + totalPaidToBroker) == (paymentParts->principalPaid + paymentParts->interestPaid + - paymentParts->feePaid), + paymentParts->feeToPay), "ripple::LoanPay::doApply", "payments add up"); - // If there is not enough first-loss capital - auto coverAvailableField = brokerSle->at(sfCoverAvailable); - auto debtTotalField = brokerSle->at(sfDebtTotal); - TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + auto debtTotalProxy = brokerSle->at(sfDebtTotal); - bool const sufficientCover = coverAvailableField >= - roundToAsset(asset, - tenthBipsOfValue(debtTotalField.value(), coverRateMinimum), - loanScale); - if (!sufficientCover) - { - // Add the fee to First Loss Cover Pool - coverAvailableField += totalPaidToBroker; - } - auto const brokerPayee = - sufficientCover ? brokerOwner : brokerPseudoAccount; - - // Decrease LoanBroker Debt by the amount paid, add the Loan value change, - // and subtract the change in the management fee - auto const vaultValueChange = valueMinusManagementFee( - asset, paymentParts->valueChange, managementFeeRate, loanScale); - // debtDecrease may be negative, increasing the debt - auto const debtDecrease = totalPaidToVault - vaultValueChange; + // Decrease LoanBroker Debt by the amount paid, add the Loan value change + // (which might be negative). debtDecrease may be negative, increasing the + // debt + auto const debtDecrease = totalPaidToVault - paymentParts->valueChange; XRPL_ASSERT_PARTS( - roundToAsset(asset, debtDecrease, loanScale) == debtDecrease, + isRounded(asset, debtDecrease, loanScale), "ripple::LoanPay::doApply", "debtDecrease rounding good"); - if (debtDecrease >= debtTotalField) - debtTotalField = 0; + // Despite our best efforts, it's possible for rounding errors to accumulate + // in the loan broker's debt total. This is because the broker may have more + // that one loan with significantly different scales. + if (debtDecrease >= debtTotalProxy) + debtTotalProxy = 0; else - debtTotalField -= debtDecrease; + debtTotalProxy -= debtDecrease; //------------------------------------------------------ // Vault object state changes view.update(vaultSle); vaultSle->at(sfAssetsAvailable) += totalPaidToVault; - vaultSle->at(sfAssetsTotal) += vaultValueChange; + vaultSle->at(sfAssetsTotal) += paymentParts->valueChange; + interestOwedProxy -= interestPaidToVault; XRPL_ASSERT_PARTS( *vaultSle->at(sfAssetsAvailable) <= *vaultSle->at(sfAssetsTotal), "ripple::LoanPay::doApply", @@ -287,16 +267,37 @@ LoanPay::doApply() "amount is sufficient"); XRPL_ASSERT_PARTS( paidToVault + paidToBroker <= paymentParts->principalPaid + - paymentParts->interestPaid + paymentParts->feePaid, + paymentParts->interestPaid + paymentParts->feeToPay, "ripple::LoanPay::doApply", "payment agreement"); + // Determine where to send the broker's fee + auto coverAvailableProxy = brokerSle->at(sfCoverAvailable); + TenthBips32 const coverRateMinimum{brokerSle->at(sfCoverRateMinimum)}; + + bool const sufficientCover = coverAvailableProxy >= + roundToAsset(asset, + tenthBipsOfValue(debtTotalProxy.value(), coverRateMinimum), + loanScale); + if (!sufficientCover) + { + // If there is not enough first-loss capital, add the fee to First Loss + // Cover Pool. Note that this moves the entire fee - it does not attempt + // to split it. The broker can Withdraw it later if they want, or leave + // it for future needs. + coverAvailableProxy += totalPaidToBroker; + } + auto const brokerPayee = + sufficientCover ? brokerOwner : brokerPseudoAccount; + +#if !NDEBUG auto const accountBalanceBefore = accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const vaultBalanceBefore = accountHolds( view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); auto const brokerBalanceBefore = accountHolds( view, brokerPayee, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); +#endif if (auto const ter = accountSend( view, @@ -315,8 +316,35 @@ LoanPay::doApply() WaiveTransferFee::Yes)) return ter; - return tesSUCCESS; +#if !NDEBUG + auto const accountBalanceAfter = + accountHolds(view, account_, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const vaultBalanceAfter = accountHolds( + view, vaultPseudoAccount, asset, fhIGNORE_FREEZE, ahIGNORE_AUTH, j_); + auto const brokerBalanceAfter = 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( + roundToAsset( + asset, + accountBalanceBefore + vaultBalanceBefore + brokerBalanceBefore, + balanceScale) == + roundToAsset( + asset, + accountBalanceAfter + vaultBalanceAfter + brokerBalanceAfter, + balanceScale), + "ripple::LoanPay::doApply", + "funds are conserved (with rounding)"); #endif + + return tesSUCCESS; } //------------------------------------------------------------------------------