Skip to content

Commit 810ffde

Browse files
committed
fix: Prevent early loan impairment and due-date manipulation
Stop impairLoan and unimpairLoan from rewriting sfNextPaymentDueDate when the amendment is active. Previously a colluding broker could repeatedly impair and unimpair an overdue loan to keep pushing the due date forward, permanently blocking default eligibility and suppressing late-interest / late-fee accrual.
1 parent 68e4fbd commit 810ffde

4 files changed

Lines changed: 338 additions & 42 deletions

File tree

include/xrpl/tx/transactors/lending/LendingHelpers.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,10 @@ computeFullPaymentInterest(
219219
std::uint32_t startDate,
220220
TenthBips32 closeInterestRate);
221221

222+
/// Returns true if the loan's next payment due date has passed.
223+
[[nodiscard]] bool
224+
isPaymentLate(ReadView const& view, SLE::const_ref loanSle);
225+
222226
namespace detail {
223227
// These classes and functions should only be accessed by LendingHelper
224228
// functions and unit tests

src/libxrpl/tx/transactors/lending/LendingHelpers.cpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ isRounded(Asset const& asset, Number const& value, std::int32_t scale)
6464
roundToAsset(asset, value, scale, Number::upward);
6565
}
6666

67+
[[nodiscard]] bool
68+
isPaymentLate(ReadView const& view, SLE::const_ref loanSle)
69+
{
70+
return hasExpired(view, loanSle->at(sfNextPaymentDueDate));
71+
}
72+
6773
namespace detail {
6874

6975
void
@@ -675,11 +681,6 @@ computeLatePayment(
675681
TenthBips16 managementFeeRate,
676682
beast::Journal j)
677683
{
678-
// Check if the due date has passed. If not, reject the payment as
679-
// being too soon
680-
if (!hasExpired(view, nextDueDate))
681-
return Unexpected(tecTOO_SOON);
682-
683684
// Calculate the penalty interest based on how long the payment is overdue.
684685
auto const latePaymentInterest = loanLatePaymentInterest(
685686
principalOutstanding, lateInterestRate, view.parentCloseTime(), nextDueDate);
@@ -1614,7 +1615,7 @@ loanMakePayment(
16141615

16151616
// -------------------------------------------------------------
16161617
// A late payment not flagged as late overrides all other options.
1617-
if (paymentType != LoanPaymentType::late && hasExpired(view, nextDueDateProxy))
1618+
if (paymentType != LoanPaymentType::late && isPaymentLate(view, loan))
16181619
{
16191620
// If the payment is late, and the late flag was not set, it's not
16201621
// valid
@@ -1708,6 +1709,10 @@ loanMakePayment(
17081709
// late payment handling
17091710
if (paymentType == LoanPaymentType::late)
17101711
{
1712+
// Check if the due date has passed. If not, reject the payment as being too soon
1713+
if (!isPaymentLate(view, loan))
1714+
return Unexpected(tecTOO_SOON);
1715+
17111716
TenthBips32 const lateInterestRate{loan->at(sfLateInterestRate)};
17121717
Number const latePaymentFee = loan->at(sfLatePaymentFee);
17131718

src/libxrpl/tx/transactors/lending/LoanManage.cpp

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ LoanManage::impairLoan(
282282
Asset const& vaultAsset,
283283
beast::Journal j)
284284
{
285+
if (view.rules().enabled(featureLendingProtocolV1_1) && !isPaymentLate(view, loanSle))
286+
{
287+
JLOG(j.warn()) << "Cannot impair a loan that is not late";
288+
return tecTOO_SOON;
289+
}
290+
285291
Number const lossUnrealized = owedToVault(loanSle);
286292

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

305310
// Update the Loan object
306311
loanSle->setFlag(lsfLoanImpaired);
307-
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
308-
if (!hasExpired(view, loanNextDueProxy))
312+
313+
if (!view.rules().enabled(featureLendingProtocolV1_1))
309314
{
310-
// loan payment is not yet late -
311-
// move the next payment due date to now
312-
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
315+
auto loanNextDueProxy = loanSle->at(sfNextPaymentDueDate);
316+
if (!isPaymentLate(view, loanSle))
317+
{
318+
// loan payment is not yet late move the next payment due date to now
319+
loanNextDueProxy = view.parentCloseTime().time_since_epoch().count();
320+
}
313321
}
314322
view.update(loanSle);
315323

@@ -346,19 +354,24 @@ LoanManage::unimpairLoan(
346354

347355
// Update the Loan object
348356
loanSle->clearFlag(lsfLoanImpaired);
349-
auto const paymentInterval = loanSle->at(sfPaymentInterval);
350-
auto const normalPaymentDueDate =
351-
std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) + paymentInterval;
352-
if (!hasExpired(view, normalPaymentDueDate))
357+
if (!view.rules().enabled(featureLendingProtocolV1_1))
353358
{
354-
// loan was unimpaired within the payment interval
355-
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
356-
}
357-
else
358-
{
359-
// loan was unimpaired after the original payment due date
360-
loanSle->at(sfNextPaymentDueDate) =
361-
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
359+
auto const paymentInterval = loanSle->at(sfPaymentInterval);
360+
auto const normalPaymentDueDate =
361+
std::max(loanSle->at(sfPreviousPaymentDueDate), loanSle->at(sfStartDate)) +
362+
paymentInterval;
363+
364+
if (!hasExpired(view, normalPaymentDueDate))
365+
{
366+
// loan was unimpaired within the payment interval
367+
loanSle->at(sfNextPaymentDueDate) = normalPaymentDueDate;
368+
}
369+
else
370+
{
371+
// loan was unimpaired after the original payment due date
372+
loanSle->at(sfNextPaymentDueDate) =
373+
view.parentCloseTime().time_since_epoch().count() + paymentInterval;
374+
}
362375
}
363376
view.update(loanSle);
364377

0 commit comments

Comments
 (0)