Add a few minor changes (#6158)

- Updates or fixes a couple of things I noticed while reviewing changes
  to the spec.
- Rename sfPreviousPaymentDate to sfPreviousPaymentDueDate.
- Make the vault asset cap check added in #6124 a little more robust:
  1. Check in preflight if the vault is _already_ over the limit.
  2. Prevent overflow when checking with the loan value. (Subtract
     instead of adding, in case the values are near maxint. Both return
     the same result. Also add a unit test so each case is covered.
This commit is contained in:
Ed Hennis
2025-12-22 17:33:44 -05:00
parent c91b9a0223
commit c81b26b09e
6 changed files with 54 additions and 14 deletions

View File

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

View File

@@ -102,7 +102,7 @@ TYPED_SFIELD(sfMutableFlags, UINT32, 53)
TYPED_SFIELD(sfStartDate, UINT32, 54)
TYPED_SFIELD(sfPaymentInterval, UINT32, 55)
TYPED_SFIELD(sfGracePeriod, UINT32, 56)
TYPED_SFIELD(sfPreviousPaymentDate, UINT32, 57)
TYPED_SFIELD(sfPreviousPaymentDueDate, UINT32, 57)
TYPED_SFIELD(sfNextPaymentDueDate, UINT32, 58)
TYPED_SFIELD(sfPaymentRemaining, UINT32, 59)
TYPED_SFIELD(sfPaymentTotal, UINT32, 60)

View File

@@ -372,7 +372,7 @@ protected:
if (auto loan = env.le(loanKeylet); env.test.BEAST_EXPECT(loan))
{
env.test.BEAST_EXPECT(
loan->at(sfPreviousPaymentDate) == previousPaymentDate);
loan->at(sfPreviousPaymentDueDate) == previousPaymentDate);
env.test.BEAST_EXPECT(
loan->at(sfPaymentRemaining) == paymentRemaining);
env.test.BEAST_EXPECT(
@@ -507,7 +507,7 @@ protected:
if (auto loan = env.le(loanKeylet); BEAST_EXPECT(loan))
{
return LoanState{
.previousPaymentDate = loan->at(sfPreviousPaymentDate),
.previousPaymentDate = loan->at(sfPreviousPaymentDueDate),
.startDate = tp{d{loan->at(sfStartDate)}},
.nextPaymentDate = loan->at(sfNextPaymentDueDate),
.paymentRemaining = loan->at(sfPaymentRemaining),
@@ -1454,7 +1454,7 @@ protected:
BEAST_EXPECT(
loan->at(sfPaymentInterval) == *loanParams.payInterval);
BEAST_EXPECT(loan->at(sfGracePeriod) == *loanParams.gracePd);
BEAST_EXPECT(loan->at(sfPreviousPaymentDate) == 0);
BEAST_EXPECT(loan->at(sfPreviousPaymentDueDate) == 0);
BEAST_EXPECT(
loan->at(sfNextPaymentDueDate) ==
startDate + *loanParams.payInterval);
@@ -3597,13 +3597,39 @@ protected:
env(tx);
env.close();
testcase("Vault maximum value exceeded");
testcase("Vault at maximum value");
env(set(issuer, broker.brokerID, principalRequest),
counterparty(lender),
interestRate(TenthBips32(10'000)),
sig(sfCounterpartySignature, lender),
fee(env.current()->fees().base * 5),
ter(tecLIMIT_EXCEEDED));
ter(tecLIMIT_EXCEEDED),
THISLINE);
},
nullptr);
testCase(
[&, this](Env& env, BrokerInfo const& broker, auto&) {
using namespace loan;
Number const principalRequest = broker.asset(1'000).value();
Vault vault{env};
auto tx = vault.set({.owner = lender, .id = broker.vaultID});
tx[sfAssetsMaximum] =
BrokerParameters::defaults().vaultDeposit +
broker.asset(1).number();
env(tx);
env.close();
testcase("Vault maximum value exceeded");
env(set(issuer, broker.brokerID, principalRequest),
counterparty(lender),
interestRate(TenthBips32(100'000)),
sig(sfCounterpartySignature, lender),
fee(env.current()->fees().base * 5),
paymentTotal(2),
paymentInterval(3600 * 24),
ter(tecLIMIT_EXCEEDED),
THISLINE);
},
nullptr);
}
@@ -3841,7 +3867,7 @@ protected:
BEAST_EXPECT(loan[sfPaymentInterval] == 60);
BEAST_EXPECT(loan[sfPeriodicPayment] == "1000000000");
BEAST_EXPECT(loan[sfPaymentRemaining] == 1);
BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDate));
BEAST_EXPECT(!loan.isMember(sfPreviousPaymentDueDate));
BEAST_EXPECT(loan[sfPrincipalOutstanding] == "1000000000");
BEAST_EXPECT(loan[sfTotalValueOutstanding] == "1000000000");
BEAST_EXPECT(!loan.isMember(sfLoanScale));
@@ -6048,7 +6074,7 @@ protected:
{
// --- PoC Summary ----------------------------------------------------
// Scenario: Borrower makes one periodic payment early (before next due)
// so doPayment sets sfPreviousPaymentDate to the (future)
// so doPayment sets sfPreviousPaymentDueDate to the (future)
// sfNextPaymentDueDate and advances sfNextPaymentDueDate by one
// interval. Borrower then immediately performs a full-payment
// (tfLoanFullPayment). Why it matters: Full-payment interest accrual

View File

@@ -1669,7 +1669,7 @@ loanMakePayment(
Number const periodicPayment = loan->at(sfPeriodicPayment);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDate);
auto prevPaymentDateProxy = loan->at(sfPreviousPaymentDueDate);
std::uint32_t const startDate = loan->at(sfStartDate);
std::uint32_t const paymentInterval = loan->at(sfPaymentInterval);

View File

@@ -374,7 +374,8 @@ LoanManage::unimpairLoan(
loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate =
std::max(loanSle->at(sfPreviousPaymentDate), loanSle->at(sfStartDate)) +
std::max(
loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) +
paymentInterval;
if (!hasExpired(view, normalPaymentDueDate))
{

View File

@@ -282,6 +282,15 @@ LoanSet::preclaim(PreclaimContext const& ctx)
if (!vault)
// Should be impossible
return tefBAD_LEDGER; // LCOV_EXCL_LINE
if (vault->at(sfAssetsMaximum) != 0 &&
vault->at(sfAssetsTotal) >= vault->at(sfAssetsMaximum))
{
JLOG(ctx.j.warn())
<< "Vault at maximum assets limit. Can't add another loan.";
return tecLIMIT_EXCEEDED;
}
Asset const asset = vault->at(sfAsset);
auto const vaultPseudo = vault->at(sfAccount);
@@ -411,8 +420,12 @@ LoanSet::doApply()
principalRequested,
properties.loanState.managementFeeDue);
if (vaultSle->at(sfAssetsMaximum) != 0 &&
vaultTotalProxy + state.interestDue > vaultSle->at(sfAssetsMaximum))
auto const vaultMaximum = *vaultSle->at(sfAssetsMaximum);
XRPL_ASSERT_PARTS(
vaultMaximum == 0 || vaultMaximum > *vaultTotalProxy,
"ripple::LoanSet::doApply",
"Vault is below maximum limit");
if (vaultMaximum != 0 && state.interestDue > vaultMaximum - vaultTotalProxy)
{
JLOG(j_.warn()) << "Loan would exceed the maximum assets of the vault";
return tecLIMIT_EXCEEDED;
@@ -600,7 +613,7 @@ LoanSet::doApply()
loan->at(sfTotalValueOutstanding) = properties.loanState.valueOutstanding;
loan->at(sfManagementFeeOutstanding) =
properties.loanState.managementFeeDue;
loan->at(sfPreviousPaymentDate) = 0;
loan->at(sfPreviousPaymentDueDate) = 0;
loan->at(sfNextPaymentDueDate) = startDate + paymentInterval;
loan->at(sfPaymentRemaining) = paymentTotal;
view.insert(loan);