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) {
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
_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();
updateReserveInterests(reserve, rateData);
IERC20(reserve.reserveAssetAddress).safeTransferFrom(
msg.sender,
reserve.reserveRTokenAddress,
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;
_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
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockCurveVault {
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 {
IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}