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;
}