Submitted on Mon Aug 05 2024 16:58:14 GMT-0400 (Atlantic Standard Time) by @Nyksx for Boost | Folks Finance
Report ID: #34122
Report type: Smart Contract
Report severity: High
Target: https://testnet.snowtrace.io/address/0xf8E94c5Da5f5F23b39399F6679b2eAb29FE3071e
Impacts:
Description
Brief/Intro
The borrow balance is not calculated correctly in the getLoanLiquidity function.
Vulnerability Details
When the executeLiquidation function is called, the prepareLiquidation function calculates violators' effective collateral and borrow balance with the getLoanLiquidity function. Then, the function checks whether the violator is liquidatable.
Copy violatorLiquidity = violatorLoan.getLoanLiquidity(pools, loanType.pools, oracleManager);
if (violatorLiquidity.effectiveCollateralValue >= violatorLiquidity.effectiveBorrowValue)
revert OverCollateralizedLoan(loansParams.violatorLoanId);
When we look at the getLoanLiquidity function, if the loan is a stable borrow, the function calculates the borrow balance with the calcStableBorrowBalance function.
Copy for (uint8 i = 0; i < poolsLength; i++) {
poolId = loan.borPools[i];
LoanManagerState.UserLoanBorrow memory loanBorrow = loan.borrows[poolId];
balance = loanBorrow.lastStableUpdateTimestamp > 0
? calcStableBorrowBalance(
loanBorrow.balance,
loanBorrow.lastInterestIndex,
loanBorrow.stableInterestRate,
block.timestamp - loanBorrow.lastStableUpdateTimestamp
)
: calcVariableBorrowBalance(
loanBorrow.balance,
loanBorrow.lastInterestIndex,
pools[poolId].getUpdatedVariableBorrowInterestIndex()
);
//.......
}
The calcStableBorrowBalance function first calculates the stableBorrowInterestIndex and then calculates the borrow balance.
Copy function calcStableBorrowBalance(
uint256 balance,
uint256 loanInterestIndex,
uint256 loanInterestRate,
uint256 stableBorrowChangeDelta
) private pure returns (uint256) {
uint256 stableBorrowInterestIndex = MathUtils.calcBorrowInterestIndex(
loanInterestRate,
loanInterestIndex,
stableBorrowChangeDelta
);
return balance.calcBorrowBalance(loanInterestIndex, stableBorrowInterestIndex);
}
However, the problem is that the function calls the calcBorrowBalance function wrongly. The updated borrow index needs to be the first function parameter, and the old borrow index needs to be the second function parameter. Like in the updateLoanBorrowInterests function.
Copy function updateLoanBorrowInterests(
LoanManagerState.UserLoanBorrow storage loanBorrow,
DataTypes.UpdateUserLoanBorrowParams memory params
) internal {
if (loanBorrow.lastStableUpdateTimestamp > 0) {
uint256 oldInterestIndex = loanBorrow.lastInterestIndex;
uint256 oldStableInterestRate = loanBorrow.stableInterestRate;
loanBorrow.lastInterestIndex = MathUtils.calcBorrowInterestIndex(
oldStableInterestRate,
oldInterestIndex,
block.timestamp - loanBorrow.lastStableUpdateTimestamp
);
loanBorrow.lastStableUpdateTimestamp = block.timestamp;
// update balance with interest
loanBorrow.balance = MathUtils.calcBorrowBalance(
loanBorrow.balance,
loanBorrow.lastInterestIndex,
oldInterestIndex
);
//........
}
As we can see in the updateLoanBorrowInterests function, it first updates the lastInterestIndex and then correctly calls the calcBorrowBalance.
Impact Details
Wrongly Calculating the borrow balance in the getLoanLiquidity can result in a lower borrow balance for the violator, which may also cause the liquidate function to revert even if the violator is in the liquidatable state.
References
Proof of concept
Proof of Concept
LoanManager.test.ts
Copy it("Test borrow balance", async () => {
const {
hub,
loanManager,
loanManagerAddress,
pools,
loanId,
accountId,
loanTypeId,
borrowAmount,
usdcStableInterestRate,
} = await loadFixture(depositEtherAndStableBorrowUSDCFixture);
const { pool, poolId } = pools.USDC;
const userLoanBefore = await loanManager.getUserLoan(loanId);
const oldBorrow = userLoanBefore[5][0];
// calculate interest
const timestamp = (await getLatestBlockTimestamp()) + getRandomInt(SECONDS_IN_HOUR);
await time.setNextBlockTimestamp(timestamp);
// Updated interest index
const newInterestIndex = calcBorrowInterestIndex(
oldBorrow.stableInterestRate,
oldBorrow.lastInterestIndex,
BigInt(timestamp) - oldBorrow.lastStableUpdateTimestamp,
true
);
// Correctly calculated borrow balance like in the updateLoanBorrowInterests function
const borrowBalance = calcBorrowBalance(oldBorrow.balance, newInterestIndex, oldBorrow.lastInterestIndex);
console.log("Correct borrow balance", borrowBalance);
// Wrongly calculated borrow balance like in the calcStableBorrowBalance function
const wrongBorrowBalance = calcBorrowBalance(oldBorrow.balance, oldBorrow.lastInterestIndex, newInterestIndex);
console.log("Wrong borrow balance", wrongBorrowBalance);
});
Last updated 4 months ago