Core Contracts

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

DoS risk in `LendingPool::deposit()/withdraw()` operations due to unchecked curve vault limits and status

Summary

The LendingPool contract does not check curve vault deposit/withdrawal limits or status before attempting operations, which could lead to DoS of core lending functions.

Vulnerability Details

The _depositIntoVault() and _withdrawFromVault() functions directly attempt vault operations without checking:

  1. If the vault is paused or not accepting deposits/withdrawals

  2. Maximum deposit/withdrawal limits

function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
@> curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}
function _withdrawFromVault(uint256 amount) internal {
@> curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}

These functions are called by _rebalanceLiquidity() which is triggered on every deposit/withdraw/borrow operation in the protocol. Causing a DoS of the protocol if interaction with the vault fails.

Impact

If the Curve vault:

  • Is paused/has other restrictions

  • Has reached deposit limits

Then core protocol functions will revert:

  1. Users cannot deposit funds as excess liquidity cannot be moved to vault

  2. Users cannot withdraw/borrow as required liquidity cannot be retrieved from vault

  3. This creates a temporary DoS of main protocol functionality

Tools Used

Manual review

Proof of Concept

First, a bug need to be fixed in the LendingPool::_depositIntoVault() contract to transfer the assets from RToken to LendingPool before attempting vault deposit:

function _depositIntoVault(uint256 amount) internal {
+ // Transfer assets from RToken to LendingPool first
+ IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

Create a new Mock CurveVault contract that inherits from ERC4626 in the file contracts/mocks/core/vaults/CurveVaultMock.sol:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC4626} from "@openzeppelin/contracts/token/ERC20/extensions/ERC4626.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {ICurveCrvUSDVault} from "../../../interfaces/curve/ICurveCrvUSDVault.sol";
contract CurveVaultMock is ERC4626 {
bool public depositEnabled;
bool public withdrawEnabled;
constructor(
address _asset
) ERC4626(IERC20(_asset)) ERC20("Mock Curve Vault", "mcrvVault") {
depositEnabled = true;
withdrawEnabled = true;
}
function enableDeposits(bool _depositEnabled) external {
depositEnabled = _depositEnabled;
}
function enableWithdrawals(bool _withdrawEnabled) external {
withdrawEnabled = _withdrawEnabled;
}
/**
* @notice Withdraws assets from the vault
* @param amount The amount of assets to withdraw
* @param owner The owner of the shares
* @param recipient The address to receive the assets
* @param maxLoss The maximum acceptable loss (not used in mock)
* @param strategies Array of strategies (not used in mock)
*/
function withdraw(
uint256 amount,
address owner,
address recipient,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256) {
// Mock implementation ignores maxLoss and strategies parameters
return super.withdraw(amount, recipient, owner);
}
/**
* @notice Returns the maximum amount that can be deposited
* @param owner The address to check deposit limit for
*/
function maxDeposit(
address owner
) public view override(ERC4626) returns (uint256) {
if (!depositEnabled) return 0;
return super.maxDeposit(owner);
}
function maxWithdraw(address owner) public view virtual override returns (uint256) {
if (!withdrawEnabled) return 0;
return super.maxWithdraw(owner);
}
function withdraw(
uint256 assets,
address receiver,
address owner
) public virtual override returns (uint256) {
// Mock implementation ignores maxLoss and strategies parameters
return super.withdraw(assets, receiver, owner);
}
}

Add the following test case to the test/unit/core/pools/LendingPool/LendingPool.test.js file:

describe("Curve Vault", function () {
let curveVault;
beforeEach(async function () {
const CurveVault = await ethers.getContractFactory("CurveVaultMock");
curveVault = await CurveVault.deploy(crvusd.target);
await lendingPool.setCurveVault(curveVault.target);
});
it("lending pool deposit DoS if curve vault deposit fails", async function () {
await curveVault.enableDeposits(false);
const depositAmount = ethers.parseEther("100");
// Given that vault deposits are disabled, the whole lending pool deposit will fail
await expect(lendingPool.connect(user1).deposit(depositAmount))
.to.be.revertedWithCustomError(curveVault, "ERC4626ExceededMaxDeposit");
});
it("lending pool withdraw DoS if curve vault deposit fails", async function () {
const depositAmount = ethers.parseEther("100");
await expect(lendingPool.connect(user1).deposit(depositAmount))
await curveVault.enableWithdrawals(false);
// Given that vault withdrawals are disabled, the whole lending pool withdraw will fail
await expect(lendingPool.connect(user1).withdraw(depositAmount))
.to.be.revertedWithCustomError(curveVault, "ERC4626ExceededMaxWithdraw");
});
});

Recommendations

Add checks before vault operations:

function _depositIntoVault(uint256 amount) internal {
+ // Check vault status and limits
+ uint256 maxDeposit = curveVault.maxDeposit(address(this));
+ if (maxDeposit == 0) return;
+ if (maxDeposit < amount) {
+ amount = maxDeposit;
+ }
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}

Similar checks should be added for withdrawals. Consider implementing fallback mechanisms when vault operations fail.

Updates

Lead Judging Commences

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

LendingPool core operations revert if Curve vault is unavailable during rebalancing, even when sufficient liquidity exists in the pool

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

LendingPool core operations revert if Curve vault is unavailable during rebalancing, even when sufficient liquidity exists in the pool

Support

FAQs

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