Core Contracts

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

Drain of reserve funds due to incorrect borrow logic in Lending Pool

Summary

Users are allowed to borrow reserve token using their RAAC NFTs as collateral. For safety reasons, the total borrow amount should be lower than the liquidation threshold, i. e., 80% of the user collateral value (using the base percentage). However, logic to enforce this condition is incorrect in the LendingPool::borrow function, allowing users to borrow up to 125% their collateral amount, resulting in a considerable loss of value for the protocol.

Vulnerability Details

Upper bound of borrow amount is calculated depending on the user collateral value. The condition to authorize a requested amount should be:

collateralValue > userTotalDebt

using the liquidation threshold percentage :

collateralValue * liquidationThreshold > userTotalDebt

or

revert if collateralValue * liquidationThreshold < userTotalDebt.

Currently, the logic to check the borrow amount in the LendingPool::borrow function is incorrect:

revert if collateralValue < userTotalDebt * liquidationThreshold

This condition allows any user to borrow up to 1/liquidationThreshold of their total NFT collateral value (125% when liquidationThreshold is 80%), resulting in significant losses for the protocol.

>LendingPool.sol
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
// @audit - comparison should be (collateralValue.percentMul(liquidationThreshold) < userTotalDebt)
@> 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);
}

Proof of Concept

Assuming that the reserve token is crvUsd and the malicious user interacts with the protocol for the first time, let's take the following example:

  • Malicious user deposit an NFT valued at 1000 crvUsd

  • He decides to borrow 1200 crvUsd (higher amount than his collateral value) by calling the LendingPool::borrow function

    • collateralValue = 1000

    • userTotalDebt = user.scaledDebtBalance.rayMul(reserve.usageIndex) + amount = 0 + 1200

    • liquidationThreshold = BASE_LIQUIDATION_THRESHOLD = 80% in basis points

    • (collateralValue < userTotalDebt.percentMul(liquidationThreshold)) -- > (1000 < (1200 * 0.8))

    • (1000 < 960) is false, then the process will continue

In this way, a malicious user managed to borrow 20% more crvUsd than the value of their collateral NFT, causing the protocol to lose its funds.

Impact

Impact: High

Likelihood: High

Tools Used

Manual Review

Recommendations

Perform a correct validation of the requested amount to borrow:

> LendingPool.sol
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();
- }
+ if (collateralValue.percentMul(liquidationThreshold) < userTotalDebt) {
+ 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 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool::borrow as well as withdrawNFT() reverses collateralization check, comparing collateral < debt*0.8 instead of collateral*0.8 > debt, allowing 125% borrowing vs intended 80%

Support

FAQs

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

Give us feedback!