Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: high
Valid

Incorrect Debt Calculation in Borrow Function Leading to Insufficient Collateral Checks

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();
// 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;
// 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);
}

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):

  1. Existing Debt: 100e18 * 1.1e27 / 1e27 = 110e18

  2. Added Debt (raw): 50e18

  3. Total Debt Checked: 110e18 + 50e18 = 160e18

Actual Debt Incurred:

  1. Scaled Borrowed Amount: 50e18 / 1.1e27 ≈ 45.4545e18

  2. New scaledDebtBalance: 100e18 + 45.4545e18 = 145.4545e18

  3. Actual Total Debt: 145.4545e18 * 1.1e27 / 1e27 ≈ 160e18

  • If the usageIndex increases to 2e27 (due to interest accrual) before the next borrow:

    • Existing Debt: 100e18 * 2e27 / 1e27 = 200e18

    • Raw Borrowed Amount Added: 50e18

    • Total Debt Checked: 200e18 + 50e18 = 250e18

    • Actual Debt Incurred:

      • Scaled Amount: 50e18 / 2e27 = 25e18

      • New scaledDebtBalance: 100e18 + 25e18 = 125e18

      • Actual Debt: 125e18 * 2e27 / 1e27 = 250e18

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);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 4 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::borrow tracks debt as user.scaledDebtBalance += scaledAmount while DebtToken mints amount+interest, leading to accounting mismatch and preventing full debt repayment

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.