Add test case to reproduce RIPD-3459

- Improve a few loan test helper functions.
- Make Loan.GracePeriod a default field.
This commit is contained in:
Ed Hennis
2025-11-08 19:04:32 -05:00
parent cc46e94308
commit 4fbf687166
2 changed files with 117 additions and 27 deletions

View File

@@ -540,7 +540,7 @@ LEDGER_ENTRY(ltLOAN, 0x0089, Loan, loan, ({
{sfOverpaymentInterestRate, soeDEFAULT}, {sfOverpaymentInterestRate, soeDEFAULT},
{sfStartDate, soeREQUIRED}, {sfStartDate, soeREQUIRED},
{sfPaymentInterval, soeREQUIRED}, {sfPaymentInterval, soeREQUIRED},
{sfGracePeriod, soeREQUIRED}, {sfGracePeriod, soeDEFAULT},
{sfPreviousPaymentDate, soeDEFAULT}, {sfPreviousPaymentDate, soeDEFAULT},
{sfNextPaymentDueDate, soeOPTIONAL}, {sfNextPaymentDueDate, soeOPTIONAL},
// The loan object tracks these values: // The loan object tracks these values:

View File

@@ -615,6 +615,19 @@ protected:
} }
case AssetType::MPT: { case AssetType::MPT: {
// Enough to cover initial fees
if (!env.le(keylet::account(issuer)))
env.fund(
env.current()->fees().accountReserve(10) * 10, issuer);
if (!env.le(keylet::account(lender)))
env.fund(
env.current()->fees().accountReserve(10) * 10,
noripple(lender));
if (!env.le(keylet::account(borrower)))
env.fund(
env.current()->fees().accountReserve(10) * 10,
noripple(borrower));
MPTTester mptt{env, issuer, mptInitNoFund}; MPTTester mptt{env, issuer, mptInitNoFund};
mptt.create( mptt.create(
{.flags = {.flags =
@@ -644,7 +657,7 @@ protected:
{ {
using namespace jtx; using namespace jtx;
Env env(*this, beast::severities::kWarning); Env env(*this);
auto const asset = createAsset( auto const asset = createAsset(
env, env,
@@ -653,15 +666,25 @@ protected:
Account("issuer"), Account("issuer"),
Account("lender"), Account("lender"),
Account("borrower")); Account("borrower"));
auto const principal = asset(loanParams.principalRequest).number();
auto const interest = loanParams.interest.value_or(TenthBips32{});
auto const interval =
loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval);
auto const total =
loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal);
auto const props = computeLoanProperties( auto const props = computeLoanProperties(
asset, asset,
asset(loanParams.principalRequest).number(), principal,
loanParams.interest.value_or(TenthBips32{}), interest,
loanParams.payInterval.value_or(LoanSet::defaultPaymentInterval), interval,
loanParams.payTotal.value_or(LoanSet::defaultPaymentTotal), total,
brokerParams.managementFeeRate, brokerParams.managementFeeRate,
asset(brokerParams.vaultDeposit).number().exponent()); asset(brokerParams.vaultDeposit).number().exponent());
log << "Loan properties:\n" log << "Loan properties:\n"
<< "\tPrincipal: " << principal << std::endl
<< "\tInterest rate: " << interest << std::endl
<< "\tPayment interval: " << interval << std::endl
<< "\tTotal Payments: " << total << std::endl
<< "\tPeriodic Payment: " << props.periodicPayment << std::endl << "\tPeriodic Payment: " << props.periodicPayment << std::endl
<< "\tTotal Value: " << props.totalValueOutstanding << std::endl << "\tTotal Value: " << props.totalValueOutstanding << std::endl
<< "\tManagement Fee: " << props.managementFeeOwedToBroker << "\tManagement Fee: " << props.managementFeeOwedToBroker
@@ -680,8 +703,7 @@ protected:
env.journal)); env.journal));
} }
std::optional< std::optional<std::tuple<BrokerInfo, Keylet, jtx::Account>>
std::tuple<BrokerInfo, Keylet, VerifyLoanStatus, jtx::Account>>
createLoan( createLoan(
jtx::Env& env, jtx::Env& env,
AssetType assetType, AssetType assetType,
@@ -707,7 +729,7 @@ protected:
env( env(
pay((asset.native() ? env.master : issuer), pay((asset.native() ? env.master : issuer),
lender, lender,
asset(brokerParams.vaultDeposit))); asset(brokerParams.vaultDeposit + brokerParams.coverDeposit)));
// Fund the borrower later once we know the total loan // Fund the borrower later once we know the total loan
// size // size
@@ -746,10 +768,7 @@ protected:
env.close(); env.close();
VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet); return std::make_tuple(broker, loanKeylet, pseudoAcct);
return std::make_tuple(
broker, loanKeylet, verifyLoanStatus, pseudoAcct);
} }
void void
@@ -919,10 +938,11 @@ protected:
broker.params.managementFeeRate); broker.params.managementFeeRate);
BEAST_EXPECT( BEAST_EXPECT(
paymentComponents.trackedValueDelta == roundedPeriodicPayment || paymentComponents.trackedValueDelta <= roundedPeriodicPayment ||
(paymentComponents.specialCase == (paymentComponents.specialCase ==
detail::PaymentSpecialCase::final && detail::PaymentSpecialCase::final &&
paymentComponents.trackedValueDelta < roundedPeriodicPayment)); paymentComponents.trackedValueDelta >=
roundedPeriodicPayment));
BEAST_EXPECT( BEAST_EXPECT(
paymentComponents.trackedValueDelta == paymentComponents.trackedValueDelta ==
paymentComponents.trackedPrincipalDelta + paymentComponents.trackedPrincipalDelta +
@@ -1092,7 +1112,9 @@ protected:
auto broker = std::get<BrokerInfo>(*loanResult); auto broker = std::get<BrokerInfo>(*loanResult);
auto loanKeylet = std::get<Keylet>(*loanResult); auto loanKeylet = std::get<Keylet>(*loanResult);
auto verifyLoanStatus = std::get<VerifyLoanStatus>(*loanResult); auto pseudoAcct = std::get<Account>(*loanResult);
VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus); makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus);
} }
@@ -6198,25 +6220,20 @@ protected:
.payInterval = 150, .payInterval = 150,
.gracePd = 0}; .gracePd = 0};
describeLoan(brokerParams, loanParams, AssetType::XRP); auto const assetType = AssetType::XRP;
describeLoan(brokerParams, loanParams, assetType);
Env env(*this, all); Env env(*this, all);
auto loanResult = createLoan( auto loanResult = createLoan(
env, env, assetType, brokerParams, loanParams, issuer, lender, borrower);
AssetType::XRP,
brokerParams,
loanParams,
issuer,
lender,
borrower);
if (!BEAST_EXPECT(loanResult)) if (!BEAST_EXPECT(loanResult))
return; return;
auto broker = std::get<BrokerInfo>(*loanResult); auto broker = std::get<BrokerInfo>(*loanResult);
auto loanKeylet = std::get<Keylet>(*loanResult); auto loanKeylet = std::get<Keylet>(*loanResult);
// auto verifyLoanStatus = std::get<VerifyLoanStatus>(*loanResult);
using tp = NetClock::time_point; using tp = NetClock::time_point;
using d = NetClock::duration; using d = NetClock::duration;
@@ -6264,12 +6281,82 @@ protected:
env.close(); env.close();
} }
void
testRIPD3459()
{
testcase("RIPD-3459 - LoanBroker incorrect debt total");
using namespace jtx;
Account const issuer("issuer");
Account const lender("lender");
Account const borrower("borrower");
BrokerParameters const brokerParams{
.vaultDeposit = 200'000,
.debtMax = 0,
.coverRateMin = TenthBips32{0},
// .managementFeeRate = TenthBips16{5919},
.coverRateLiquidation = TenthBips32{0}};
LoanParameters const loanParams{
.account = lender,
.counter = borrower,
.principalRequest = Number{100'000, -4},
.interest = TenthBips32{100'000},
.payTotal = 10,
// Guess
// .payInterval = 10,
.gracePd = 0};
auto const assetType = AssetType::MPT;
describeLoan(brokerParams, loanParams, assetType);
Env env(*this, all);
auto loanResult = createLoan(
env, assetType, brokerParams, loanParams, issuer, lender, borrower);
if (!BEAST_EXPECT(loanResult))
return;
auto broker = std::get<BrokerInfo>(*loanResult);
auto loanKeylet = std::get<Keylet>(*loanResult);
auto pseudoAcct = std::get<Account>(*loanResult);
VerifyLoanStatus verifyLoanStatus(env, broker, pseudoAcct, loanKeylet);
if (auto const brokerSle = env.le(broker.brokerKeylet());
BEAST_EXPECT(brokerSle))
{
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
{
BEAST_EXPECT(
brokerSle->at(sfDebtTotal) ==
loanSle->at(sfTotalValueOutstanding));
}
}
makeLoanPayments(env, broker, loanParams, loanKeylet, verifyLoanStatus);
if (auto const brokerSle = env.le(broker.brokerKeylet());
BEAST_EXPECT(brokerSle))
{
if (auto const loanSle = env.le(loanKeylet); BEAST_EXPECT(loanSle))
{
log << pretty(brokerSle->getJson()) << std::endl
<< pretty(loanSle->getJson()) << std::endl;
BEAST_EXPECT(
brokerSle->at(sfDebtTotal) ==
loanSle->at(sfTotalValueOutstanding));
}
}
}
public: public:
void void
run() override run() override
{ {
testRIPD3831();
#if LOANTODO #if LOANTODO
testCoverDepositAllowsNonTransferableMPT(); testCoverDepositAllowsNonTransferableMPT();
testLoanPayLateFullPaymentBypassesPenalties(); testLoanPayLateFullPaymentBypassesPenalties();
@@ -6305,6 +6392,9 @@ public:
testLoanNextPaymentDueDateOverflow(); testLoanNextPaymentDueDateOverflow();
testRequireAuth(); testRequireAuth();
testRIPD3831();
testRIPD3459();
} }
}; };