Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions include/xrpl/tx/transactors/lending/LendingHelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,10 @@ computeFullPaymentInterest(
std::uint32_t startDate,
TenthBips32 closeInterestRate);

/// Returns true if the loan's next payment due date has passed.
[[nodiscard]] bool
isPaymentLate(ReadView const& view, SLE::const_ref loanSle);

namespace detail {
// These classes and functions should only be accessed by LendingHelper
// functions and unit tests
Expand Down
17 changes: 11 additions & 6 deletions src/libxrpl/tx/transactors/lending/LendingHelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale)
roundToAsset(asset, value, scale, Number::upward);
}

[[nodiscard]] bool
isPaymentLate(ReadView const& view, SLE::const_ref loanSle)
{
return hasExpired(view, loanSle->at(sfNextPaymentDueDate));
}

namespace detail {

void
Expand Down Expand Up @@ -675,11 +681,6 @@ computeLatePayment(
TenthBips16 managementFeeRate,
beast::Journal j)
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

computeLatePayment lacks its own timing guard — any future caller bypasses tecTOO_SOON enforcement. Restore the precondition check inside the function:

// Check if the due date has passed. If not, reject the payment as
// being too soon
if (!hasExpired(view, nextDueDate))
return Unexpected(tecTOO_SOON);

// Calculate the penalty interest based on how long the payment is overdue.
auto const latePaymentInterest = loanLatePaymentInterest(
principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate);
Expand Down Expand Up @@ -1614,7 +1615,7 @@ loanMakePayment(

// -------------------------------------------------------------
// A late payment not flagged as late overrides all other options.
if (paymentType != LoanPaymentType::late && hasExpired(view, nextDueDateProxy))
if (paymentType != LoanPaymentType::late && isPaymentLate(view, loan))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isPaymentLate called twice on the same SLE — cache the result to avoid future TOCTOU-style divergence:

    bool const isLate = isPaymentLate(view, loan);
    if (paymentType != LoanPaymentType::late && isLate)

{
// If the payment is late, and the late flag was not set, it's not
// valid
Expand Down Expand Up @@ -1708,6 +1709,10 @@ loanMakePayment(
// late payment handling
if (paymentType == LoanPaymentType::late)
{
// Check if the due date has passed. If not, reject the payment as being too soon
if (!isPaymentLate(view, loan))
return Unexpected(tecTOO_SOON);

TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
Number const latePaymentFee = loan->at(sfLatePaymentFee);

Expand Down
51 changes: 32 additions & 19 deletions src/libxrpl/tx/transactors/lending/LoanManage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,12 @@ LoanManage::impairLoan(
Asset const& vaultAsset,
beast::Journal j)
{
if (view.rules().enabled(featureLendingProtocolV1_1) && !isPaymentLate(view, loanSle))
{
JLOG(j.warn()) << "Cannot impair a loan that is not late";
return tecTOO_SOON;
}

Number const lossUnrealized = owedToVault(loanSle);

// The vault may be at a different scale than the loan. Reduce rounding
Expand All @@ -296,20 +302,22 @@ LoanManage::impairLoan(
{
// Having a loss greater than the vault's unavailable assets
// will leave the vault in an invalid / inconsistent state.
JLOG(j.warn()) << "Vault unrealized loss is too large, and will "
"corrupt the vault.";
JLOG(j.warn()) << "Vault unrealized loss is too large, and will corrupt the vault.";
return tecLIMIT_EXCEEDED;
}
view.update(vaultSle);

// Update the Loan object
loanSle->setFlag(lsfLoanImpaired);
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!hasExpired(view, loanNextDueProxy))

if (!view.rules().enabled(featureLendingProtocolV1_1))
{
// loan payment is not yet late -
// move the next payment due date to now
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
if (!isPaymentLate(view, loanSle))
{
// loan payment is not yet late move the next payment due date to now
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
}
}
view.update(loanSle);

Expand Down Expand Up @@ -346,19 +354,24 @@ LoanManage::unimpairLoan(

// Update the Loan object
loanSle->clearFlag(lsfLoanImpaired);
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate =
std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) + paymentInterval;
if (!hasExpired(view, normalPaymentDueDate))
if (!view.rules().enabled(featureLendingProtocolV1_1))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unimpairLoan under featureLendingProtocolV1_1 skips advancing sfNextPaymentDueDate, leaving the loan perpetually late and re-impairable immediately. Advance the due date after clearing the impaired flag:

{
// loan was unimpaired within the payment interval
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
}
else
{
// loan was unimpaired after the original payment due date
loanSle->at(sfNextPaymentDueDate) =
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
auto const paymentInterval = loanSle->at(sfPaymentInterval);
auto const normalPaymentDueDate =
std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) +
paymentInterval;

if (!hasExpired(view, normalPaymentDueDate))
{
// loan was unimpaired within the payment interval
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
}
else
{
// loan was unimpaired after the original payment due date
loanSle->at(sfNextPaymentDueDate) =
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
}
}
view.update(loanSle);

Expand Down
Loading
Loading