Core Contracts

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

Liquidity Mismatch in Borrow and Withdraw Function Causing Unnecessary Transaction Reverts

Summary

The issue arises when a user borrows or liqidity provider withdraws reserve assets while there is insufficient liquidity in the protocol. The system is designed to withdraw liquidity from the Curve vault to fulfill the borrow request. However, there is a mismatch in where the liquidity is withdrawn to and how it is subsequently transferred to the borrower.

Vulnerability Details

  1. Borrow Function Flow(LendingPool::borrow):

  • The user initiates a borrow request.

  • _ensureLiquidity(amount) is called to check available liquidity.

  • If liquidity is insufficient, _withdrawFromVault(amount) is executed to withdraw funds from the Curve vault.

  • Issue: The withdrawn funds are sent to address(this), which is the lending pool contract, NOT the RToken contract.

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);
.....

LendingPool::_ensureLiquidity:

function _ensureLiquidity(uint256 amount) internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) {
return;
}
uint256 availableLiquidity = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (availableLiquidity < amount) {
uint256 requiredAmount = amount - availableLiquidity;
// Withdraw required amount from the Curve vault
@> _withdrawFromVault(requiredAmount);
}
}

LendingPool::_withdrawFromVault:

function _withdrawFromVault(uint256 amount) internal {
@> curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}

2.Transfer Mismatch:

  • After ensuring liquidity, the function LendingPool::borrow attempts to transfer the borrowed amount to the user:

IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);

RToken::transferAsset:

function transferAsset(address user, uint256 amount) external override onlyReservePool {
IERC20(_assetAddress).safeTransfer(user, amount);
}
  • This function transfers funds from the RToken contract to the borrower.

  • Problem: If liquidity was insufficient before the borrow call, the required funds were withdrawn to the Lending Pool, but the transfer still attempts to send them from the RToken contract.

  • This results in a revert due to insufficient balance in the RToken contract, even though the required liquidity exists in the Lending Pool.

Apart from the above the liquidity providers who had initally deposited asset tokens can also call LendingPool::withdraw to burn their rtokens and get their amount of asset tokens back but even here the _ensureLiquidty is called so if liquidity is less tokens are again transferred to lending pool but to the user tokens are transfeered from RToken contract here.

function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (withdrawalsPaused) revert WithdrawalsArePaused();
// Update the reserve state before the withdrawal
ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
@> _ensureLiquidity(amount);
// Perform the withdrawal through ReserveLibrary
(uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve, // ReserveData storage
rateData, // ReserveRateData storage
amount, // Amount to withdraw
msg.sender // Recipient
);
// Rebalance liquidity after withdrawal
_rebalanceLiquidity();
emit Withdraw(msg.sender, amountWithdrawn);
}

Impact

  1. Borrow transactions will fail when liquidity is low, even though liquidity is correctly withdrawn from Curve Vault.

  2. The system fails to efficiently utilize Curve Vault liquidity, leading to poor capital efficiency.

  3. Users attempting to withdraw their liquidity will experience transaction failures when the RToken contract has insufficient balance.

  4. Even though liquidity was successfully withdrawn from Curve Vault, users cannot access their funds due to the same misalignment in transfer sources.

Tools Used

Manual

Recommendations

  1. Modify _withdrawFromVault to transfer the withdrawn liquidity directly to the RToken contract, ensuring it has sufficient balance to process the borrow transaction.

function _withdrawFromVault(uint256 amount) internal {
- curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
+ curveVault.withdraw(amount, reserve.reserveRTokenAddress, msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}
Updates

Lead Judging Commences

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

LendingPool::_depositIntoVault and _withdrawFromVault don't transfer tokens between RToken and LendingPool, breaking Curve vault interactions

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

LendingPool::_depositIntoVault and _withdrawFromVault don't transfer tokens between RToken and LendingPool, breaking Curve vault interactions

Support

FAQs

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