Core Contracts

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

The deposit will always revert if curveVault is enabled

Summary

The LendingPool contract allows users to deposit assets, which are transferred to the reserve.reserveRTokenAddress (RToken contract). After a deposit, the _rebalanceLiquidity function is called to manage liquidity by potentially depositing excess assets into a Curve Vault. However, there is a critical vulnerability in the _depositIntoVault function: it attempts to deposit assets into the Curve Vault directly from the LendingPool contract, but the assets are stored in the RToken contract (reserve.reserveRTokenAddress), not in the LendingPool contract. This results in a failed transaction because the LendingPool contract does not hold the assets required for the deposit.

Vulnerability Details

The deposit function in LendingPool calls ReserveLibrary.deposit.

function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// Update the reserve state before the deposit
ReserveLibrary.updateReserveState(reserve, rateData);
// Perform the deposit through ReserveLibrary
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
// Rebalance liquidity after deposit
_rebalanceLiquidity();
emit Deposit(msg.sender, amount, mintedAmount);
}

In ReserveLibrary.deposit, the user's assets are transferred to the RToken contract (reserve.reserveRTokenAddress)

function deposit(ReserveData storage reserve,ReserveRateData storage rateData,uint256 amount,address depositor) internal returns (uint256 amountMinted) {
if (amount < 1) revert InvalidAmount();
// Update reserve interests
updateReserveInterests(reserve, rateData);
// Transfer asset from caller to the RToken contract
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender, // from
reserve.reserveRTokenAddress, // to
amount // amount
);
...
...

After the deposit, _rebalanceLiquidity is called to manage liquidity. In _rebalanceLiquidity, if the current buffer (currentBuffer) exceeds the desired buffer (desiredBuffer), the excess assets are deposited into the Curve Vault by calling _depositIntoVault.

if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
_depositIntoVault(excess);
}

The _depositIntoVault function attempts to deposit assets into the Curve Vault:

function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

However, the assets are stored in the RToken contract (reserve.reserveRTokenAddress), not in the LendingPool contract. As a result, the curveVault.deposit call fails because the LendingPool contract does not have the required assets.

POC

add the following MockCurveVault into the repo

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockCurveVault {
// CrvUSD token interface
IERC20 public reserveAssetToken;
constructor(IERC20 token) {
reserveAssetToken = token;
}
/**
* @notice Deposits assets into the vault
* @param assets Amount of assets to deposit
* @param receiver Address to receive the shares
* @return shares Amount of shares minted
*/
function deposit(uint256 assets, address receiver) external returns (uint256 shares){
reserveAssetToken.transferFrom(msg.sender,address(this),assets);
}
/**
* @notice Withdraws assets from the vault
* @param assets Amount of assets to withdraw
* @param receiver Address to receive the assets
* @param owner Owner of the shares
* @param maxLoss Maximum acceptable loss in basis points
* @param strategies Optional specific strategies to withdraw from
* @return shares Amount of shares burned
*/
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares){
reserveAssetToken.transfer(receiver,assets);
}
}

It will transfer the asset from msg.sender to the vault. Then add following test case into LendingPool.test.js

describe("_rebalanceLiquidity may revert if curveValut is set", function () {
it("set curveValut and deposit", async function () {
const MockCurveValutContract = await ethers.getContractFactory("MockCurveVault");
let curveVault = await MockCurveValutContract.deploy(crvusd.target);
await lendingPool.connect(owner).setCurveVault(await curveVault.getAddress());
const depositAmount = ethers.parseEther("1000");
await crvusd.connect(user1).approve(lendingPool.target, depositAmount);
await lendingPool.connect(user1).deposit(depositAmount);
});
});

run npx hardhat test --grep "set curveValut and deposit"

LendingPool
_rebalanceLiquidity may revert if curveValut is set
1) set curveValut and deposit
0 passing (19s)
1 failing
1) LendingPool
_rebalanceLiquidity may revert if curveValut is set
set curveValut and deposit:
Error: VM Exception while processing transaction: reverted with custom error 'ERC20InsufficientBalance("0x1F585372F116E1055AF2bED81a808DDf9638dCCD", 0, 1600000000000000000000)'
at crvUSDToken.burnFrom (contracts/mocks/core/tokens/crvUSDToken.sol:33)
at crvUSDToken._transfer (@openzeppelin/contracts/token/ERC20/ERC20.sol:178)
at crvUSDToken.transferFrom (@openzeppelin/contracts/token/ERC20/ERC20.sol:157)
at MockCurveVault.deposit (contracts/mocks/core/pools/MockCurveVault.sol:18)
at LendingPool._depositIntoVault (contracts/core/pools/LendingPool/LendingPool.sol:801)
at LendingPool._rebalanceLiquidity (contracts/core/pools/LendingPool/LendingPool.sol:785)
at LendingPool.deposit (contracts/core/pools/LendingPool/LendingPool.sol:233)

The deposit function will revert due to ERC20InsufficientBalance error.

Impact

  • The _depositIntoVault function will always fail when attempting to deposit assets into the Curve Vault.

  • This prevents the _rebalanceLiquidity function from correctly managing liquidity, potentially leading to inefficiencies in the protocol's liquidity management.

  • Users may experience unexpected behavior when depositing assets, as the protocol's liquidity rebalancing mechanism is broken.

The impact is High, the likelihood is Medium, so the severity is High

Tools Used

Manual Review

Recommendations

To fix this issue, the _depositIntoVault function must first transfer the assets from the RToken contract to the LendingPool contract before depositing them into the Curve Vault. For example:

function _depositIntoVault(uint256 amount) internal {
// Transfer assets from RToken contract to LendingPool contract
IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
// Approve Curve Vault to spend the assets
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
// Deposit assets into Curve Vault
curveVault.deposit(amount, address(this));
// Update total deposits in the vault
totalVaultDeposits += amount;
}
Updates

Lead Judging Commences

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