Boost _ Folks Finance 33817 - [Smart Contract - High] Incorrect calculation of effective borrow value in getLoanLiquidity leads to protocol insolvency through wrong withdrawals and liquidations
Submitted on Tue Jul 30 2024 04:21:26 GMT-0400 (Atlantic Standard Time) by @zarkk for Boost | Folks Finance
Effective stable borrow value is calculated incorrectly with stable borrow balance to be decreasing instead of increasing, leading to protocol insolvency due to under-collateralized loan acceptance and wrong liquidations.
Vulnerability Details
When a withdrawal or a liquidation is about to happen in Folks Finance, the collateralization of the UserLoan is checked through the getLoanLiquidity function which returns the effective borrow value and the effective collateral value of a UserLoan. We can see the implementation here :
However, if we deep on the calculations of stable borrow balance, we will see that they are done incorrectly. In order to calculate the stable borrow balance, calcStableBorrow function is called passing the current debt (loanBorrow.balance), the current interest index(loanBorrow.lastInterestIndex), the current interest rate (loanBorrow.stableInterestRate) and the time passed since the last update. This happens in order to update the interest index and then the debt to be updated also, so to reflect the present. Let's see how calcStableBorrowBalance is implemented :
The stableborrowInterestIndex is the new updated interest index which is supposed to inflate the balance. However, as we see in the calcBorrowBalance, due to wrong order of parameters, the balance is decreased instead of increased. Here, is the calcBorrowBalance of MathUtils which expects as second parameter the new interest index and as third parameter the old interest index.
However, the parameters are passed in the opposite order and a deflated balance (debt) is returned. As a result, the UserLoan seems to have less debt than what really has.
Impact Details
This vulnerability leads, eventually and rapidly, to the insolvency of Folks Finance. Firstly, as demonstrated in the Proof of Concept (PoC), users are able to withdraw collateral from their loans, leaving them under-collateralized. This can have catastrophic effects for the protocol as it accrues bad debt. Secondly, it prevents legitimate liquidations from happening since the debt of the violator seems smaller than it actually is. The combined effect of these issues can lead to a cascading failure of the lending system, where the protocol cannot cover the borrowed amounts with the available collateral, ultimately leading to insolvency and significant financial losses for both the protocol and its users.
To understand better this critical vulnerability, add this test under the "Withdraw" test suite of LoanManager.test.tsand runnpm run test``` :
it.only("Should successfuly withdraw and leave user loan under-collateralised",async () => {const { hub,loanManager,loanManagerAddress,pools,loanId,accountId } =awaitloadFixture( depositEtherAndStableBorrowUSDCFixture );// Deposited 1 ETH = $3,000// Borrowed stable 1,000 USDC = $1,000// stableInterestRate = BigInt(0.1e18) => 10%const { pool,poolId } =pools.ETH;time.increase(31536000); // Advance 1 year.constdepositInterestIndex=BigInt(1.05e18);awaitpool.setUpdatedDepositInterestIndex(depositInterestIndex);// Now, with depositInterestIndex to equal 1.05, our 1 ETH deposit -> 1.05 ETH = $3,150 -> 70% CF = $2,100// Now, with stableInterestRate = 0.1 -> 1,000 USDC = $1,100 debt. // That means that if we try to withdraw 0.6 ETH = $1,260, we will leave in the protocol $2,100 - $1,260 = $840 and the loan will be under-collateralised.
// So, it should revert with under-collateralised error, but it is not.// Withdraw 0.6 ETH = $1,260 and leave the loan under-collateralised.let withdrawAmount =BigInt(0.6e18);let withdrawFAmount =toFAmount(withdrawAmount, depositInterestIndex);awaitpool.setWithdrawPoolParams({ underlingAmount: withdrawAmount, fAmount: withdrawFAmount });constwithdraw=loanManager.connect(hub).withdraw(loanId, accountId, poolId, withdrawAmount,false); });