-
Notifications
You must be signed in to change notification settings - Fork 11
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
There is no way to liquidate a position if it breaches maxDebtPerCollateralToken value creating bad debt. #1057
Comments
The issue is very well demonstrated, properly formatted, contains a coded POC. To the attention of the Judge; The submission also refers to the submission #153 as maxDebtPerCollateral comparison holds the relative value of the collateral. |
0xSorryNotSorry marked the issue as high quality report |
0xSorryNotSorry marked the issue as primary issue |
Confirming this, thanks for the high quality of the report! On one hand, I think it might be considered a governance issue if the chosen term parameters allow loans to grow into unsafe territory, and that the situation is handled properly in the current implementation because GUILD holders can offboard the term if any loan of the term is unsafe, but on the other hand, I don't think it's a large code change to allow loans to be called if they violate the "max debt check that is in _borrow" during their lifetime, it is an elegant addition to the codebase that we'll probably do, so I think it's worth including in the audit report. |
eswak (sponsor) confirmed |
Trumpero marked the issue as satisfactory |
Trumpero removed the grade |
Trumpero marked the issue as satisfactory |
Trumpero marked the issue as selected for report |
Hi @Trumpero, |
@kazantseff
The configs in your scenario requires a large interest rate and repayment duration, resulting in growing interest higher than Secondly, there are only 2 cases that can cause a loan to breach However, I understand your concern since your report has a high quality and your recommended mitigation was accepted. I will reevaluate the quality of each duplicate and decrease the partial credit if needed. |
@Trumpero |
Lines of code
https://github.com/code-423n4/2023-12-ethereumcreditguild/blob/2376d9af792584e3d15ec9c32578daa33bb56b43/src/loan/LendingTerm.sol#L634-L675
Vulnerability details
Impact
Liquidations in the system are done via
LendingTerm._call()
, which will auction the loan's collateral to repay outstanding debt. A loan can be called only if the term has been offboarded or if a loan missed a periodic partialRepay.To understand the issue, we must understand how the loan is created and how it can be called.
First, when a loan is created through a call to
_borrow()
, the contract checks if the borrowAmount is lesser than the maxBorrow:It calcualates maxBorrow using
params.maxDebtPerCollateralToken
. For example, if maxDebtPerCollateralToken is 2000e18, then with 15e18 tokens of collateral, a user can borrow up to 30_000 of CREDIT tokens.Then it's important to understand how liquidations work in the system. If we were to look at
_call()
function we could notice that it only allows liquidations in two specific cases:It will only allow to call a position if a term is depreceated or if the loan missed periodic partial repayment.
This approach creates problems and opens up a griefing attack vector.
There are few ways it can go wrong. Let's firstly discuss an issue that will arise even for a non-malicious user.
In order for a user to not get liquidated, he must call
partialRepay()
before a specific deadline set in term's parameters. If we look at the_partialRepay()
function:We can see, that it enforces user to repay at least
params.minPartialRepayPercent
, which may not always be enough for a position to stay "healthy". By "healthy" I mean a position that does not breachmaxDebtPerCollateralToken
value, which is a parameter of a LendingTerm.Imagine a scenario:
User borrows 30_000 CREDIT with 15 TOKENS of collateral. His debtPerCollateral value is
30_000 / 15 = 2000
, which is exactly equal to maxDebtPerCollateralToken.Now a year has passed, the loanDebt (debt + interest) is 34_500, a user is obligated to repay at least
34_500 * 0.1 = 3450
. After partial repayment his debtPerCollateral value is(34500 - 3450) / 15 = 2070
. While he breached the maxDebtPerCollateralToken value, his position is not callable, because he did not miss a periodic partial repayment.Now let's talk about a potential malicious behaviour that is encouraged in the current implementation.
Periodic partial repayments are not enforced for every term, they may or may not be enabled, so the only condition for a liquidation in this case is a depreceated term.
This means that basically every position is essentially "unliquitable", because
partialRepayDelayPassed()
will always return false in such case:A malicious user can abuse this by not repaying his loan or by not adding collateral to his loan when interest accrues above maxDebtPerCollateralToken.
There will be no way to do anything with such positions, the only possible solution would be to offboard a full term. This will obviously damage the protocol, as offboarding a term means calling every position, which subsequently increases a chance of a loss occuring. Also by offboarding a term, lenders will miss out on interest, because every position is force-closed.
Proof of Concept
Here is the PoC demonstrating this issue, for the sake of simplicity I did not change the protocol's test suite configuration, but I just wanted to show that this is possible with any parameters (even the ones expected by the team), because a loan can still be partially repaid even if it missed partial repay deadline (I mention this earlier in my report, saying that this is the reason that makes this situation even easier to occur).
Please add this function to LendingTerm.t.sol and run it with
forge test --match-test 'testBreakMaxDebtPerCollateralToken' -vv
.In conclusion I want to say that parameters of a LendingTerm are only limited to a logical sort of degree, i.e:
Because of this the situation is bound to occur. It's better to enforce strict rules on the smart contract level.
Tools Used
Manual review
Recommended Mitigation Steps
If periodic partial repays are turned on, the possible solution would be to enforce the maxDebtPerCollateral check in _partialRepay, that will enforce users to repay an amount that would make debtPerCollateralToken value lesser than maxDebtPerCollateralToken value.
Something like this would be sufficent enough:
In case if partial repays are turned on it's important to have this check in
_partialRepay()
rather than in_call()
, because otherwise almost every position would immediately go underwater and be liquidatable as soon as it gets opened if a user borrowed up to the maximum amount, but it's fine to have debtPerCollateralToken greater than maxDebtPerCollateralToken until user makes a partialRepay.In case if periodic partial repays are turned off, the check must be in
_call()
, since there will be no partial repays. Check if debtPerCollateralToken value is lesser than maxDebtPerCollateral token, otherwise liquidate a position, but that would mean that users won't be able to borrow up to maxDebtPerCollateral, since their position will immediatelly go underwater, but it seems to be a matter of trade-offs. This the potential way to modify_call()
.Assessed type
Other
The text was updated successfully, but these errors were encountered: