Open c4-submissions opened 11 months ago
minhquanym marked the issue as duplicate of #592
MarioPoneder marked the issue as satisfactory
MarioPoneder marked the issue as selected for report
We decided to permit borrowers to close markets if delinquent and just zero out state.timeDelinquent
, returning everything outstanding at that moment and ending things there. It's preferable from the POV of the lender to just be able to access their notional and interest ASAP rather than wait for the timer to run back down to within the grace period. There's no alternative 'good' solution to this that isn't particularly fiddly given the way in which interest compounds.
The suggested solution would require that people waited until the market was out of delinquency before the scale factor caught up when only the penalty rate applied so they could redeem for the extra amount due to them: this could be a long time if the situation leading up to a market closure after going delinquent is a protracted one.
Mitigated by https://github.com/wildcat-finance/wildcat-protocol/pull/57/commits/292ebda50008062dac0c5acbffe45639143fadd1
Was selected for report because of overall quality and level of detail.
laurenceday (sponsor) confirmed
laurenceday marked the issue as disagree with severity
laurenceday marked the issue as agree with severity
Inside me are two wolves, apparently
Lines of code
https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarket.sol#L142 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarket.sol#L151-L154 https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/libraries/FeeMath.sol#L168-L172
Vulnerability details
To explain this issue, I will need to mention two things: the fee structure of the protocol and how closing a market works. Let's start with the fees.
Lenders earn interest with two different types of fees: Base interest and delinquency fee. The base interest depends on the annual interest rate of the market and it is paid by the borrower no matter what. On the other hand, the delinquency fee is a penalty fee and it is paid by the borrower if the reserves of the market drop below the required reserves amount.
The important part is how the penalty fees are calculated and I'll be focusing on penalty fees at the moment.
Every market has a delinquency grace period, which is a period that is not penalized. If a market is delinquent but the grace period is not passed yet, there is no penalty fee. After the grace period is passed, the penalty fee is applied.
The most crucial part starts now: The penalty fee does not become 0 immediately after the delinquency is cured. The penalty fee is still being applied even after the delinquency is cured until the grace tracker counts down to zero.
An example from the protocol gitbook/borrowers section is: "Note: this means that if a markets grace period is 3 days, and it takes 5 days to cure delinquency, this means that 4 days of penalty APR are paid."
Here you can find the code snippet of penalty fee calculation:
https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/libraries/FeeMath.sol#L89
Now, let's check how to close a market and here is the
closeMarket()
function:https://github.com/code-423n4/2023-10-wildcat/blob/c5df665f0bc2ca5df6f06938d66494b11e7bdada/src/market/WildcatMarket.sol#L142C1-L161C4
While closing the market, the total debt is calculated and the required amount is transferred to the market. This way all debts are covered. However, the covered total debt is calculated with the current scale factor. As you can see above, this function does not check if there are still penalties to be paid. It should have checked the
state.timeDelinquent
.If the
state.timeDelinquent > grace period
when closing the market (which means the borrower still has more penalties to pay), the scale factor will keep increasing after every state update.The borrower didn't pay the remaining penalties when closing the market, but who will pay it?
Lenders will keep earning those penalty fees (the base rate will be 0 after closing, but the penalty fee will still accumulate)
Lenders will start withdrawing their funds.
All lenders except the last one will withdraw
the exact debt to the lender when closed + the penalty fee after closing
.The last lender will not even be able to withdraw
the exact debt to the lender when closed
because some portion of the funds dedicated to the last lender are already transferred to the previous lenders as the penalty fee.The borrower might intentionally do it to escape from the penalty, or the borrower may not even be aware of the situation.
The borrower had a cash flow problem after taking the debt
The market stayed delinquent for a long time
The borrower found some funds
The borrower wanted to close the high-interest debts right after finding some funds
Immediately paid everything and closed the market while the market was still delinquent.
From the borrower's perspective, they paid all of their debt while closing the market.
But in reality, the borrower only paid the half of the penalty fee (while the counter was counting up). But the second half of the penalties, which will be accumulated while the counter was counting down, is not paid by the borrower.
The protocol does not check if there are remaining penalties, and doesn't charge the borrower enough while closing the market.
I provided a coded PoC below that shows every step of the vulnerability.
Impact
Borrowers who are aware of this may create charming markets with lower base rate but higher penalty rate (They know they won't pay the half of it). Or the borrowers may not be aware of this, but the protocol doesn't take the required penalty from them. They "unintentionally" not pay the penalty, but the lender will have to cover it.
Proof of Concept
Coded PoC
You can use the protocol's own test setup to prove this issue.
- Copy the snippet below, and paste it into the
WildcatMarket.t.sol
test file.- Run it with
forge test --match-test test_closeMarket_withoutPaying_HalfofThePenalty -vvv
Below, you can find the test results:
Tools Used
Manuel review, Foundry
Recommended Mitigation Steps
I think there might be two different solutions: Easier one and the other one.
The easy solution is just not to allow the borrower to close the market until all the penalty fees are accumulated. This can easily be done by checking
state.timeDelinquent
in thecloseMarket()
function.That one is simple, but I don't think it is fair for the borrower because the borrower will have to pay the base rate too for that additional amount of time. Maybe the borrower will be inclined to pay the
current debt + future penalties
and close the market as soon as possible.That's why I think closing the market can still be allowed even if there are penalties to accumulate. However, the problem with that one is we can not know the exact amount of future penalties due to the compounding mechanism. It will depend on how many times the state is updated while the grace counter counts down.
Therefore I believe a buffer amount should be added. If the borrowers want to close the market, they should pay
current debt + expected future penalties + buffer amount
, and the excess amount from the buffer should be transferred back to the borrower after every lender withdraws their funds.Assessed type
Context