Core Contracts

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

When ensuring there is sufficient liquidity for lending, the reserve assets are transferred to the incorrect address

Summary

When a user borrows reserve assets via borrow()in LendingPool.sol, the source of these reserve assets is the Rtoken contract. However, when the _ensureLiquidity() function called within the borrow() function ensures there is sufficient liquidity to be able to lend to the borrower, the Curve Vault transfers the reserve assets to LendingPool.sol instead of the the RToken contract.

Vulnerability Details

Looking at the flow of borrow()function in LendingPool.sol, assume this scenario:

  1. Alice calls borrow() and wishes to borrow 100,000 crvUSD

  2. Alice's collateral value is retrieved via getUserCollateralValue()

  3. Reserve states are updated via ReserveLibrary.updateReserveState()

  4. Function ensures there is sufficient liquidity via _ensureLiquidity()

    1. Now, let's assume available liquidity in rToken address = 50,000 crvUSD

      uint256 availableLiquidity = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
      if (availableLiquidity < amount) {
      uint256 requiredAmount = amount - availableLiquidity;
      // Withdraw required amount from the Curve vault
      _withdrawFromVault(requiredAmount);
      }
    2. since availableLiquidity < 100_000e18, _withdrawFromVault()is called

      function _withdrawFromVault(uint256 amount) internal {
      curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
      totalVaultDeposits -= amount;
      }
    3. amount (which is 50_000e18) is withdrawn from the Curve Vault to address(this), which is LendingPool.sol

  5. Back to the borrow()function, function checks if Alice has sufficient collateral to cover her debt

  6. DebtTokens are minted to Alice to represent her 100,000 crvUSD debt

  7. Now, 100,000 crvUSD is transferred to Alice:

    1. Rtoken.transferAsset()is called

      IRToken(reserve.reserveRTokenAddress).transferAsset(msg.sender, amount);
    2. 100,000 crvUSD is being transferred from RToken address to Alice

      function transferAsset(address user, uint256 amount) external override onlyReservePool {
      IERC20(_assetAddress).safeTransfer(user, amount);
      }
    3. This transfer will fail, because RToken address still has only 50,000 crvUSD. Alice will not get her borrowed 100,000 crvUSD

LOC

[_withdrawFromVault] (https://github.com/Cyfrin/2025-02-raac/blob/89ccb062e2b175374d40d824263a4c0b601bcb7f/contracts/core/pools/LendingPool/LendingPool.sol#L809-L812)


Impact

This misallocation of liquidity can result in severe disruptions in lending operations. LendingPool.sol will receive crvUSD from the Curve vault, but the available liquidity is measured by the balance of crvUSD in the RToken contract. This leads to excessive top-ups of crvUSD to LendingPool.sol, and yet user cannot borrow crvUSD due to insufficient reserve assets in RToken contract.

Tools Used

Manual

Recommendations

In _withdrawFromVault(), change the recipient of crvUSD from address(this)to reserve.reserveRTokenAddress.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 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 7 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.

Give us feedback!