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:
If the vault is paused or not accepting deposits/withdrawals
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:
Then core protocol functions will revert:
Users cannot deposit funds as excess liquidity cannot be moved to vault
Users cannot withdraw/borrow as required liquidity cannot be retrieved from vault
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
:
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) {
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) {
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");
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);
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.