Users flagged for liquidation can mistakenly make partial repayments, believing they can restore their health factor, only to later lose all repayments when liquidation is finalized. Additionally, even if a user fully repays their debt but forgets to call closeLiquidation()
, they can still lose all collateralized NFTs, which the liquidator can claim for free or at an extremely low cost.
Furthermore, repayments continue to be allowed even after the grace period expires, exacerbating user losses. Lastly, even full repayments can leave a small dust amount that grows due to compounding interest, potentially making closeLiquidation()
revert at the last moment.
This creates a scenario where users waste their funds on ineffective repayments and/or can still lose their NFTs due to a missing an/or on time finalization step.
Partial Repayment Does Not Prevent Liquidation
A user has been flagged for liquidation by someone via initiateLiquidation()
:
Thinking that improving their health factor is sufficient to avoid liquidation, as is commonly practiced by many other protocols, they proceed to making partial repayments via repay()
that does not stop them from making this dire mistake.
The user naively queries calculateHealthFactor()
and is happy their healthFactor
is now greater than or equal to healthFactorLiquidationThreshold
(1e18 by default).
However, healthFactor
is no longer relevant once liquidation is initiated. As a matter of fact, the only time calculateHealthFactor()
getting triggered in LendingPool.sol is when someone calling initiateLiquidation()
to initiate the liquidation process if a user's health factor is below the threshold:
If the full amount is not repaid, liquidation can still be finalized after the grace period. As soon as the user is aware of this rule, they may not have enough resources to repay the difference. The add on loss incurred will be devastating if the user has made a significant sum of repayment, which will all be put to drain and happily arbitraged by the liquidator.
Full Repayment Without Calling closeLiquidation() Still Results in NFT Loss
Even if a user fully repays their debt, they must manually call closeLiquidation()
. If they fail to do so, the liquidator can still finalize liquidation and claim all of the user’s NFTs for free or for as little as 1e6 units of debt.
This is because finalizeLiquidation()
does not care whether the debt was already repaid (partially or fully with non-zero dust entailed or healthFactor >= healthFactorLiquidationThreshold
), allowing the liquidator to to receive valuable NFTs as enlisted in user.nftTokenIds
.
Repayments Are Allowed Even After Grace Period Expires
Users who routinely make small repayments (e.g., weekly, daily or even hourly using a giro automatic schedule) may unknowingly and unnecessarily continue paying down their debt even after the grace period has expired.
These repayments are effectively and exacerbatingly wasted since liquidation will still be finalized at any moment. The repayment will revert only after the liquidator has called finalizeLiquidation()
when user.scaledDebtBalance
is reduced to 0
.
Compounding Interest Can Prevent Liquidation Closure
Even if a user fully repays their debt, a small dust amount may remain.
If the grace period is long (e.g., 7 days as allowed by the upper setting limit), the remaining dust can grow past 1e6 due to exponentially compounding interest, making closeLiquidation()
revert at the last moment.
Wasted Repayments: Users may unknowingly make partial repayments, only to lose all funds when liquidation is finalized.
Unfair NFT Seizure: Users who fully repay but forget to close liquidation will lose their NFTs for free or at a minimal cost to the liquidator.
Bad UX & Loss of Trust: Users may incorrectly assume that repaying enough to restore their health factor prevents liquidation, leading to frustration and lack of confidence in the protocol.
Unexpected Liquidation Failures: Dust accumulation due to compounding interest may make closeLiquidation()
impossible to execute, preventing users from exiting liquidation even after full repayment.
Manual
Enforce Full Repayment If Under Liquidation
Modify _repay()
to check isUnderLiquidation[user]
, and if true, require the user to repay their debt in full (i.e., ensure userDebt <= DUST_THRESHOLD
after repayment).
Automatically Call closeLiquidation()
After Full Repayment
Change closeLiquidation()
from external to internal.
Modify _repay()
to automatically trigger closeLiquidation()
if the user is flagged for liquidation and the user’s debt is fully repaid.
Prevent Repayments After Grace Period Expires
Modify _repay()
to reject repayments if liquidation grace period has expired.
Ensure finalizeLiquidation()
Checks for Outstanding Debt
Before allowing the liquidator to seize NFTs, verify that the user's debt is nonzero.
If the user’s debt is already cleared to zero or near zero and is still flagged for liquidation, modify finalizeLiquidation()
to transfer all NFT back to the user.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.