fix: Avoid principal-zeroing in non-final loan payments at coarse scale (#7050)

Co-authored-by: Ed Hennis <ed@ripple.com>
This commit is contained in:
Vito Tumas
2026-05-21 16:46:26 +02:00
committed by GitHub
parent f6fd5ddb0a
commit 795dc5e364
2 changed files with 278 additions and 127 deletions

View File

@@ -1058,11 +1058,22 @@ computePaymentComponents(
rules, periodicPayment, periodicRate, paymentRemaining - 1, managementFeeRate);
// Round the target to the loan's scale to match how actual loan values
// are stored.
// are stored. With fixCleanup3_2_0 enabled, principal is rounded upward
// and interest downward so that at coarse scale principal sticks at the
// floor (until the final payment clears it) while interest absorbs each
// periodic payment. Without the amendment the pre-existing round-to-
// nearest behavior is preserved (which can hit the "Partial principal
// payment" assertion on degenerate integer-scale loans).
bool const fixCleanup320Enabled = rules.enabled(fixCleanup3_2_0);
Number::RoundingMode const principalRounding =
fixCleanup320Enabled ? Number::RoundingMode::Upward : Number::getround();
Number::RoundingMode const interestRounding =
fixCleanup320Enabled ? Number::RoundingMode::Downward : Number::getround();
LoanState const roundedTarget = LoanState{
.valueOutstanding = roundToAsset(asset, trueTarget.valueOutstanding, scale),
.principalOutstanding = roundToAsset(asset, trueTarget.principalOutstanding, scale),
.interestDue = roundToAsset(asset, trueTarget.interestDue, scale),
.principalOutstanding =
roundToAsset(asset, trueTarget.principalOutstanding, scale, principalRounding),
.interestDue = roundToAsset(asset, trueTarget.interestDue, scale, interestRounding),
.managementFeeDue = roundToAsset(asset, trueTarget.managementFeeDue, scale)};
// Get the current actual loan state from the ledger values

View File

@@ -69,6 +69,7 @@
#include <cstdint>
#include <cstdlib>
#include <functional>
#include <initializer_list>
#include <limits>
#include <map>
#include <optional>
@@ -90,8 +91,25 @@ class Loan_test : public beast::unit_test::Suite
protected:
// Ensure that all the features needed for Lending Protocol are included,
// even if they are set to unsupported.
FeatureBitset const all_{jtx::testableAmendments()};
// All 2^N permutations of `all_` with each subset of the given features
// excluded. The first entry is always `all_` itself (empty exclusion);
// the last excludes every feature in the list.
std::vector<FeatureBitset>
amendmentCombinations(std::initializer_list<uint256> features) const
{
std::vector<FeatureBitset> result{all_};
for (auto const& f : features)
{
auto const n = result.size();
for (std::size_t i = 0; i < n; ++i)
result.push_back(result[i] - f);
}
return result;
}
std::string const iouCurrency_{"IOU"};
void
@@ -1201,7 +1219,8 @@ protected:
runLoan(
AssetType assetType,
BrokerParameters const& brokerParams,
LoanParameters const& loanParams)
LoanParameters const& loanParams,
FeatureBitset features)
{
using namespace jtx;
@@ -1209,7 +1228,7 @@ protected:
Account const lender("lender");
Account const borrower("borrower");
Env env(*this, all_);
Env env(*this, features);
auto loanResult =
createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
@@ -2896,7 +2915,7 @@ protected:
}
void
testLoanSet()
testLoanSet(FeatureBitset features)
{
using namespace jtx;
@@ -2915,7 +2934,7 @@ protected:
std::function<void(Env&, BrokerInfo const&, MPTTester&)> mptTest,
std::function<void(Env&, BrokerInfo const&)> iouTest,
CaseArgs args = {}) {
Env env(*this, all_);
Env env(*this, features);
env.fund(XRP(args.initialXRP), issuer, lender, borrower);
env.close();
if (args.requireAuth)
@@ -3453,14 +3472,14 @@ protected:
}
void
testLifecycle()
testLifecycle(FeatureBitset features)
{
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.
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
@@ -3547,7 +3566,7 @@ protected:
}
void
testSelfLoan()
testSelfLoan(FeatureBitset features)
{
testcase << "Self Loan";
@@ -3555,7 +3574,7 @@ protected:
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.
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
// For simplicity, lender will be the sole actor for the vault &
@@ -3682,7 +3701,7 @@ protected:
}
void
testBatchBypassCounterparty()
testBatchBypassCounterparty(FeatureBitset features)
{
// From FIND-001
testcase << "Batch Bypass Counterparty";
@@ -3693,7 +3712,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env(*this, features);
Account const lender{"lender"};
Account const borrower{"borrower"};
@@ -3749,14 +3768,14 @@ protected:
}
void
testWrongMaxDebtBehavior()
testWrongMaxDebtBehavior(FeatureBitset features)
{
// From FIND-003
testcase << "Wrong Max Debt Behavior";
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -3795,7 +3814,7 @@ protected:
}
void
testLoanPayComputePeriodicPaymentValidRateInvariant()
testLoanPayComputePeriodicPaymentValidRateInvariant(FeatureBitset features)
{
// From FIND-012
testcase << "LoanPay xrpl::detail::computePeriodicPayment : "
@@ -3803,7 +3822,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -3863,7 +3882,7 @@ protected:
}
void
testRPC()
testRPC(FeatureBitset features)
{
// This will expand as more test cases are added. Some functionality
// is tested in other test functions.
@@ -3871,7 +3890,7 @@ protected:
using namespace jtx;
Env env(*this, all_);
Env env(*this, features);
auto lowerFee = [&]() {
// Run the local fee back down.
@@ -4542,7 +4561,7 @@ protected:
}
void
testAccountSendMptMinAmountInvariant()
testAccountSendMptMinAmountInvariant(FeatureBitset features)
{
// (From FIND-006)
testcase << "LoanSet trigger xrpl::accountSendMPT : minimum amount "
@@ -4550,7 +4569,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -4602,7 +4621,7 @@ protected:
}
void
testLoanPayDebtDecreaseInvariant()
testLoanPayDebtDecreaseInvariant(FeatureBitset features)
{
// From FIND-007
testcase << "LoanPay xrpl::LoanPay::doApply : debtDecrease "
@@ -4611,7 +4630,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
using namespace Lending;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -4695,14 +4714,14 @@ protected:
}
void
testLoanPayComputePeriodicPaymentValidTotalInterestInvariant()
testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(FeatureBitset features)
{
// From FIND-010
testcase << "xrpl::loanComputePaymentParts : valid total interest";
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -4902,7 +4921,7 @@ protected:
}
void
testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant()
testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(FeatureBitset features)
{
// From FIND-009
testcase << "xrpl::loanComputePaymentParts : totalPrincipalPaid "
@@ -4911,7 +4930,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
using namespace Lending;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -5004,7 +5023,7 @@ protected:
}
void
testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant()
testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(FeatureBitset features)
{
// From FIND-008
testcase << "xrpl::loanComputePaymentParts : loanValueChange rounded";
@@ -5012,7 +5031,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
using namespace Lending;
Env env(*this, all_);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -5089,7 +5108,7 @@ protected:
}
void
testLoanNextPaymentDueDateOverflow()
testLoanNextPaymentDueDateOverflow(FeatureBitset features)
{
// For FIND-013
testcase << "Prevent nextPaymentDueDate overflow";
@@ -5097,7 +5116,7 @@ protected:
using namespace jtx;
using namespace std::chrono_literals;
using namespace Lending;
Env env(*this, all_);
Env env{*this, features};
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -5621,14 +5640,14 @@ protected:
#if LOAN_TODO
void
testLoanPayLateFullPaymentBypassesPenalties()
testLoanPayLateFullPaymentBypassesPenalties(FeatureBitset features)
{
testcase("LoanPay full payment skips late penalties");
using namespace jtx;
using namespace loan;
using namespace std::chrono_literals;
Env env(*this, all);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -5774,7 +5793,7 @@ protected:
}
void
testLoanCoverMinimumRoundingExploit()
testLoanCoverMinimumRoundingExploit(FeatureBitset features)
{
auto testLoanCoverMinimumRoundingExploit = [&, this](Number const& principalRequest) {
testcase << "LoanBrokerCoverClawback drains cover via rounding"
@@ -5784,7 +5803,7 @@ protected:
using namespace loan;
using namespace loanBroker;
Env env(*this, all);
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -5860,7 +5879,7 @@ protected:
#endif
void
testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic()
testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(FeatureBitset features)
{
// --- PoC Summary ----------------------------------------------------
// Scenario: Borrower makes one periodic payment early (before next due)
@@ -5882,7 +5901,7 @@ protected:
using namespace loan;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env{*this, features};
Account const lender{"poc_lender4"};
Account const borrower{"poc_borrower4"};
@@ -6085,13 +6104,13 @@ protected:
}
void
testDustManipulation()
testDustManipulation(FeatureBitset features)
{
testcase("Dust manipulation");
using namespace jtx;
using namespace std::chrono_literals;
Env env(*this, all_);
Env env{*this, features};
// Setup: Create accounts
Account const issuer{"issuer"};
@@ -6225,7 +6244,7 @@ protected:
}
void
testRIPD3831()
testRIPD3831(FeatureBitset features)
{
using namespace jtx;
@@ -6252,7 +6271,7 @@ protected:
auto const assetType = AssetType::XRP;
Env env(*this, all_);
Env env{*this, features};
auto loanResult =
createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
@@ -6298,7 +6317,7 @@ protected:
}
void
testRIPD3459()
testRIPD3459(FeatureBitset features)
{
testcase("RIPD-3459 - LoanBroker incorrect debt total");
@@ -6323,7 +6342,7 @@ protected:
auto const assetType = AssetType::MPT;
Env env(*this, all_);
Env env{*this, features};
auto loanResult =
createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
@@ -6423,14 +6442,14 @@ protected:
}
void
testRoundingAllowsUndercoverage()
testRoundingAllowsUndercoverage(FeatureBitset features)
{
testcase("Minimum cover rounding allows undercoverage (XRP)");
using namespace jtx;
using namespace loanBroker;
Env env(*this, all_);
Env env{*this, features};
Account const lender{"lender"};
Account const borrower{"borrower"};
@@ -6502,7 +6521,7 @@ protected:
}
void
testRIPD3902()
testRIPD3902(FeatureBitset features)
{
testcase("RIPD-3902 - 1 IOU loan payments");
@@ -6529,7 +6548,7 @@ protected:
auto const assetType = AssetType::IOU;
Env env(*this, all_);
Env env{*this, features};
auto loanResult =
createLoan(env, assetType, brokerParams, loanParams, issuer, lender, borrower);
@@ -6673,7 +6692,7 @@ protected:
}
void
testIssuerIsBorrower()
testIssuerIsBorrower(FeatureBitset features)
{
testcase("RIPD-4096 - Issuer as borrower");
@@ -6693,7 +6712,7 @@ protected:
auto const assetType = AssetType::IOU;
Env env(*this, all_);
Env env{*this, features};
auto loanResult =
createLoan(env, assetType, brokerParams, loanParams, issuer, lender, issuer);
@@ -6790,14 +6809,14 @@ protected:
}
void
testOverpaymentManagementFee()
testOverpaymentManagementFee(FeatureBitset features)
{
testcase("testOverpaymentManagementFee");
using namespace jtx;
using namespace loan;
Env env(*this, all_);
Env env{*this, features};
Account const lender{"lender"}, borrower{"borrower"};
@@ -6843,7 +6862,7 @@ protected:
}
void
testLoanPayBrokerOwnerMissingTrustline()
testLoanPayBrokerOwnerMissingTrustline(FeatureBitset features)
{
testcase << "LoanPay Broker Owner Missing Trustline (PoC)";
using namespace jtx;
@@ -6852,7 +6871,7 @@ protected:
Account const borrower("borrower");
Account const broker("broker");
auto const iou = issuer["IOU"];
Env env(*this, all_);
Env env(*this, features);
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
// Set up trustlines and fund accounts
@@ -6911,7 +6930,7 @@ protected:
}
void
testLoanPayBrokerOwnerUnauthorizedMPT()
testLoanPayBrokerOwnerUnauthorizedMPT(FeatureBitset features)
{
testcase << "LoanPay Broker Owner MPT unauthorized";
using namespace jtx;
@@ -6921,7 +6940,7 @@ protected:
Account const borrower("borrower");
Account const broker("broker");
Env env(*this, all_);
Env env{*this, features};
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
@@ -6992,7 +7011,7 @@ protected:
}
void
testLoanPayBrokerOwnerNoPermissionedDomainMPT()
testLoanPayBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
{
testcase << "LoanPay Broker Owner without permissioned domain of the MPT";
using namespace jtx;
@@ -7002,7 +7021,7 @@ protected:
Account const borrower("borrower");
Account const broker("broker");
Env env(*this, all_);
Env env{*this, features};
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
@@ -7095,7 +7114,7 @@ protected:
}
void
testLoanSetBrokerOwnerNoPermissionedDomainMPT()
testLoanSetBrokerOwnerNoPermissionedDomainMPT(FeatureBitset features)
{
testcase << "LoanSet Broker Owner without permissioned domain of the MPT";
using namespace jtx;
@@ -7105,7 +7124,7 @@ protected:
Account const borrower("borrower");
Account const broker("broker");
Env env(*this, all_);
Env env{*this, features};
env.fund(XRP(20'000), issuer, broker, borrower);
env.close();
@@ -7167,7 +7186,7 @@ protected:
}
void
testSequentialFLCDepletion()
testSequentialFLCDepletion(FeatureBitset features)
{
testcase << "First-Loss Capital Depletion on Sequential Defaults";
@@ -7175,7 +7194,7 @@ protected:
using namespace loan;
using namespace loanBroker;
Env env(*this, all_);
Env env{*this, features};
Account const issuer{"issuer"};
Account const lender{"lender"};
@@ -7427,7 +7446,7 @@ protected:
// loss from an impaired loan, ensuring the invariant check properly
// accounts for the loss.
void
testWithdrawReflectsUnrealizedLoss()
testWithdrawReflectsUnrealizedLoss(FeatureBitset features)
{
using namespace jtx;
using namespace loan;
@@ -7446,7 +7465,7 @@ protected:
static constexpr std::uint32_t kLocalPaymentInterval = 600;
static constexpr std::uint32_t kLocalPaymentTotal = 2;
Env env(*this, all_);
Env env{*this, features};
// Setup accounts
Account const issuer{"issuer"};
@@ -7553,6 +7572,110 @@ protected:
attemptWithdrawShares(depositorB, sharesLpB, tesSUCCESS);
}
// Regression for the dual-rounding fix at coarse (integer-MPT) scale.
//
// Loan: P=1, r=50% (50000 tenth-bips), n=3, yearly interval. The
// amortization schedule produces a fractional principal
// (~0.47) which under round-to-nearest collapses to 0 in a single
// step, causing `doPayment`'s strict `>` assertion on principal to
// fire mid-loan. With fixCleanup3_2_0 enabled, principal is rounded
// upward (sticks at 1 across the first two periods) and only clears
// in the final payment.
//
// The test pays one period at a time across three LoanPay
// transactions and verifies the loan completes (paymentRemaining=0)
// with totals matching the loan's economics (1 principal + 2 interest).
void
testIntegerScalePrincipalSticks(FeatureBitset features)
{
// Without fixCleanup3_2_0, this behavior will abort the server, so
// don't run without it.
if (!features[fixCleanup3_2_0])
return;
testcase("edge: integer MPT principal stuck mid-loan completes via final");
using namespace jtx;
Env env(*this, features);
Account const issuer{"issuer"};
Account const lender{"lender"};
Account const borrower{"borrower"};
env.fund(XRP(100'000), issuer, lender, borrower);
env.close();
MPTTester mptt{env, issuer, kMptInitNoFund};
mptt.create({.maxAmt = 100'000, .flags = tfMPTCanTransfer});
PrettyAsset const asset{mptt.issuanceID()};
mptt.authorize({.account = lender});
mptt.authorize({.account = borrower});
env(pay(issuer, lender, asset(10'000)));
env(pay(issuer, borrower, asset(10'000)));
env.close();
Vault const vault{env};
auto [vaultTx, vaultKeylet] = vault.create({.owner = lender, .asset = asset});
env(vaultTx);
env.close();
env(vault.deposit({.depositor = lender, .id = vaultKeylet.key, .amount = asset(5'000)}));
env.close();
auto const brokerKeylet = keylet::loanbroker(lender.id(), env.seq(lender));
env(loanBroker::set(lender, vaultKeylet.key),
loanBroker::kDebtMaximum(Number{100}),
Fee(env.current()->fees().base * 2));
env.close();
auto const brokerStateBefore = env.le(brokerKeylet);
if (!BEAST_EXPECT(brokerStateBefore))
return;
auto const loanSequence = brokerStateBefore->at(sfLoanSequence);
auto const loanKeylet = keylet::loan(brokerKeylet.key, loanSequence);
env(loan::set(borrower, brokerKeylet.key, Number{1}),
Sig(sfCounterpartySignature, lender),
loan::kInterestRate(TenthBips32{50'000}),
loan::kPaymentTotal(3),
loan::kPaymentInterval(31'536'000),
Fee(env.current()->fees().base * 2));
env.close();
auto const borrowerStart = env.balance(borrower, asset).value();
// Three separate periodic payments of 1 each. Expected per-period
// evolution at integer MPT scale (TVO = PO + interestDue +
// managementFeeDue):
// start: PO=1, TVO=3, paymentRemaining=3
// after pay #1: PO=1, TVO=2, paymentRemaining=2 (principal sticks)
// after pay #2: PO=1, TVO=1, paymentRemaining=1 (principal sticks)
// after pay #3: PO=0, TVO=0, paymentRemaining=0 (final clears)
std::array<Number, 3> const expectedPO{Number{1}, Number{1}, Number{0}};
std::array<Number, 3> const expectedTVO{Number{2}, Number{1}, Number{0}};
std::array<std::uint32_t, 3> const expectedRemaining{2, 1, 0};
for (int i = 0; i < 3; ++i)
{
env(loan::pay(borrower, loanKeylet.key, asset(1)), Ter(tesSUCCESS));
env.close();
auto const sle = env.le(loanKeylet);
if (!BEAST_EXPECT(sle))
return;
BEAST_EXPECT(sle->at(sfPrincipalOutstanding) == expectedPO[i]);
BEAST_EXPECT(sle->at(sfTotalValueOutstanding) == expectedTVO[i]);
BEAST_EXPECT(sle->at(sfPaymentRemaining) == expectedRemaining[i]);
}
// Borrower paid 3 total regardless of fee split (1 principal + 2
// interest+fee, matching loan economics).
auto const borrowerEnd = env.balance(borrower, asset).value();
BEAST_EXPECT(borrowerStart - borrowerEnd == asset(3).value());
}
// A near-zero interest rate on a 100 USD loan
// produces total interest of ~6 units at loanScale -9. Numerical error
// in the amortization formula pushes the theoretical principal above
@@ -7735,74 +7858,91 @@ protected:
to_string(tolerance));
}
void
runAmendmentIndependent()
{
testDisabled();
testInvalidLoanSet();
testInvalidLoanDelete();
testInvalidLoanManage();
testInvalidLoanPay();
testIssuerLoan();
testServiceFeeOnBrokerDeepFreeze();
testRequireAuth();
testRIPD3901();
testBorrowerIsBroker();
testLimitExceeded();
for (auto const flags : {0u, tfLoanOverpayment})
testYieldTheftRounding(flags);
testBugInterestDueDeltaCrash();
testFullLifecycleVaultPnLNearZeroRate();
}
// Tests run under each entry in amendmentCombinations().
void
runAmendmentSensitive(FeatureBitset features)
{
#if LOAN_TODO
testLoanPayLateFullPaymentBypassesPenalties(features);
testLoanCoverMinimumRoundingExploit(features);
#endif
// Lifecycle
testSelfLoan(features);
testLoanSet(features);
testLifecycle(features);
// Payment paths
testWithdrawReflectsUnrealizedLoss(features);
testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic(features);
testBatchBypassCounterparty(features);
testLoanNextPaymentDueDateOverflow(features);
testCoverDepositWithdrawNonTransferableMPT(features);
testSequentialFLCDepletion(features);
// Invariants
testLoanPayComputePeriodicPaymentValidRateInvariant(features);
testAccountSendMptMinAmountInvariant(features);
testLoanPayDebtDecreaseInvariant(features);
testWrongMaxDebtBehavior(features);
testLoanPayComputePeriodicPaymentValidTotalInterestInvariant(features);
testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant(features);
testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant(features);
// RPC
testRPC(features);
// Edge / rounding
testDustManipulation(features);
testRoundingAllowsUndercoverage(features);
testOverpaymentManagementFee(features);
testIssuerIsBorrower(features);
testIntegerScalePrincipalSticks(features);
// RIPD regressions
testRIPD3831(features);
testRIPD3459(features);
testRIPD3902(features);
// Broker-owner permissions
testLoanPayBrokerOwnerMissingTrustline(features);
testLoanPayBrokerOwnerUnauthorizedMPT(features);
testLoanPayBrokerOwnerNoPermissionedDomainMPT(features);
testLoanSetBrokerOwnerNoPermissionedDomainMPT(features);
}
public:
void
run() override
{
#if LOAN_TODO
testLoanPayLateFullPaymentBypassesPenalties();
testLoanCoverMinimumRoundingExploit();
#endif
for (auto const flags : {0u, tfLoanOverpayment})
{
testYieldTheftRounding(flags);
}
testBugInterestDueDeltaCrash();
testFullLifecycleVaultPnLNearZeroRate();
testWithdrawReflectsUnrealizedLoss();
testInvalidLoanSet();
auto const all = jtx::testableAmendments();
testCoverDepositWithdrawNonTransferableMPT(all);
testCoverDepositWithdrawNonTransferableMPT(all - featureMPTokensV2);
testCoverDepositWithdrawNonTransferableMPT(all - fixCleanup3_2_0);
runAmendmentIndependent();
testLoanSetBlockedLoanPayAllowedWhenCanTransferCleared();
testLendingCanTradeClearedNoImpact();
testPoCUnsignedUnderflowOnFullPayAfterEarlyPeriodic();
testDisabled();
testSelfLoan();
testIssuerLoan();
testLoanSet();
testLifecycle();
testServiceFeeOnBrokerDeepFreeze();
testRPC();
testInvalidLoanDelete();
testInvalidLoanManage();
testInvalidLoanPay();
testBatchBypassCounterparty();
testLoanPayComputePeriodicPaymentValidRateInvariant();
testAccountSendMptMinAmountInvariant();
testLoanPayDebtDecreaseInvariant();
testWrongMaxDebtBehavior();
testLoanPayComputePeriodicPaymentValidTotalInterestInvariant();
testDosLoanPay(all | fixCleanup3_1_3);
testDosLoanPay(all - fixCleanup3_1_3);
testLoanPayComputePeriodicPaymentValidTotalPrincipalPaidInvariant();
testLoanPayComputePeriodicPaymentValidTotalInterestPaidInvariant();
testLoanNextPaymentDueDateOverflow();
testRequireAuth();
testDustManipulation();
testRIPD3831();
testRIPD3459();
testRIPD3901();
testRIPD3902();
testRoundingAllowsUndercoverage();
testBorrowerIsBroker();
testIssuerIsBorrower();
testLimitExceeded();
testOverpaymentManagementFee();
testLoanPayBrokerOwnerMissingTrustline();
testLoanPayBrokerOwnerUnauthorizedMPT();
testLoanPayBrokerOwnerNoPermissionedDomainMPT();
testLoanSetBrokerOwnerNoPermissionedDomainMPT();
testSequentialFLCDepletion();
testDosLoanPay(all_ | fixCleanup3_1_3);
testDosLoanPay(all_ - fixCleanup3_1_3);
for (auto const& features : amendmentCombinations({fixCleanup3_2_0, featureMPTokensV2}))
runAmendmentSensitive(features);
}
};
@@ -7867,7 +8007,7 @@ protected:
.payInterval = payInterval,
};
runLoan(assetType, brokerParams, loanParams);
runLoan(assetType, brokerParams, loanParams, all_);
}
public:
@@ -7926,7 +8066,7 @@ class LoanArbitrary_test : public LoanBatch_test
.payTotal = 2,
.payInterval = 200};
runLoan(AssetType::XRP, brokerParams, loanParams);
runLoan(AssetType::XRP, brokerParams, loanParams, all_);
}
};