Summary
In the borrow function in the LendingPool contract the calculation of the user's
total debt after borrowing. This miscalculation allows users to borrow more than their collateral should permit, leading to undercollateralized positions and potential protocol insolvency.
Vulnerability Details
The issue is from the improper scaling of the borrowed amount when checking collateral sufficiency. Specifically, the raw borrowed amount is added to the existing debt without converting it using the usageIndex
, leading to an underestimation of the total debt and thereby over-leveraged positions.
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);
}
POC
Current usageIndex
: 1.1e27 (indicating a 10% increase from the initial 1e27)
User's existing scaledDebtBalance
: 100e18 (scaled units)
User borrows amount
: 50e18 (raw units)
Incorrect Calculation (Current Code):
Existing Debt: 100e18 * 1.1e27 / 1e27 = 110e18
Added Debt (raw): 50e18
Total Debt Checked: 110e18 + 50e18 = 160e18
Actual Debt Incurred:
Scaled Borrowed Amount: 50e18 / 1.1e27 ≈ 45.4545e18
New scaledDebtBalance
: 100e18 + 45.4545e18 = 145.4545e18
Actual Total Debt: 145.4545e18 * 1.1e27 / 1e27 ≈ 160e18
Impact
Users can borrow amounts that exceed their collateral coverage when interest rates (usageIndex
) rise, increasing default risk.
Tools Used
Manual Review
Recommendations
Modify the borrow
function to scale the new borrowed amount using the current usageIndex
before adding it to the existing debt. This ensures the collateral check accurately reflects the debt's scaled value.
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();
// Update reserve state before borrowing
ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
_ensureLiquidity(amount);
// Fetch user's total debt after borrowing
- uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount;
// Correctly scale the borrowed amount
+ uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
+ uint256 userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + scaledAmount.rayMul(reserve.usageIndex);
// Ensure the user has enough collateral to cover the new debt
if (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) {
revert NotEnoughCollateralToBorrow();
}
// Update user's scaled debt balance
uint256 scaledAmount = amount.rayDiv(reserve.usageIndex);
// Mint DebtTokens to the user (scaled amount)
(bool isFirstMint, uint256 amountMinted, uint256 newTotalSupply) = IDebtToken(reserve.reserveDebtTokenAddress).mint(msg.sender, msg.sender, amount, reserve.usageIndex);
// Transfer borrowed amount to user
IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
user.scaledDebtBalance += scaledAmount;
// reserve.totalUsage += amount;
reserve.totalUsage = newTotalSupply;
// Update liquidity and interest rates
ReserveLibrary.updateInterestRatesAndLiquidity(reserve, rateData, 0, amount);
// Rebalance liquidity after borrowing
_rebalanceLiquidity();
emit Borrow(msg.sender, amount);
}