-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix: Prevent early loan impairment and due-date manipulation #6557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: tapanito/lending-fix-amendment
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -675,11 +681,6 @@ computeLatePayment( | |
| TenthBips16 managementFeeRate, | ||
| beast::Journal j) | ||
| { | ||
| // 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); | ||
|
|
@@ -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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| // If the payment is late, and the late flag was not set, it's not | ||
| // valid | ||
|
|
@@ -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); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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); | ||
|
|
||
|
|
@@ -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)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| // 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); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
computeLatePaymentlacks its own timing guard — any future caller bypassestecTOO_SOONenforcement. Restore the precondition check inside the function: