Summary
The LendingPool attempts to deposit assets into curveVault, but the assets are held in the RToken contract, preventing successful deposits.
Vulnerability Details
When a user deposits assets, they are transferred to the RToken contract:
/contracts/core/pools/LendingPool/LendingPool.sol:233
233: function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
234:
235: ReserveLibrary.updateReserveState(reserve, rateData);
236:
237:
238: uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
239:
240:
241: _rebalanceLiquidity();
242:
243: emit Deposit(msg.sender, amount, mintedAmount);
244: }
The assets are transferred to RToken contract. which can be seen in below code:
/contracts/libraries/pools/ReserveLibrary.sol:329
329:
330: IERC20(reserve.reserveAssetAddress).safeTransferFrom(
331: msg.sender,
332: reserve.reserveRTokenAddress,
333: amount
334: );
During _rebalanceLiquidity, the LendingPool determines whether to deposit in to or withdraw from curveVault:
/contracts/core/pools/LendingPool/LendingPool.sol:823
823: uint256 totalDeposits = reserve.totalLiquidity;
824: uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
825: uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
826:
827: if (currentBuffer > desiredBuffer) {
828: uint256 excess = currentBuffer - desiredBuffer;
829:
830: _depositIntoVault(excess);
831: } else if (currentBuffer < desiredBuffer) {
832: uint256 shortage = desiredBuffer - currentBuffer;
833:
834: _withdrawFromVault(shortage);
835: }
836:
However, in _depositIntoVault, the LendingPool grants approval for the reserve asset but lacks ownership of the funds:
/contracts/core/pools/LendingPool/LendingPool.sol:858
858: function _depositIntoVault(uint256 amount) internal {
860: IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
861: curveVault.deposit(amount, address(this));
862: totalVaultDeposits += amount;
863: }
Since assets reside in the RToken contract, not the LendingPool, the deposit fails.
POC
Add this mock CurveCrvUSDVault:
/contracts/mocks/crvVaultMock.sol
import {ICurveCrvUSDVault} from "../interfaces/curve/ICurveCrvUSDVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract MockUsdVault is ICurveCrvUSDVault {
address assetAddress;
constructor(address _assetAddress) {
assetAddress = _assetAddress;
}
function deposit(
uint256 assets,
address receiver
) external returns (uint256 shares) {
IERC20(assetAddress).transferFrom(msg.sender, address(this), assets);
return shares;
}
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares) {
IERC20(assetAddress).transfer(msg.sender, assets);
return shares;
}
function asset() external view returns (address) {
return address(this);
}
function totalAssets() external view returns (uint256) {
return 0;
}
function pricePerShare() external view returns (uint256) {
return 1e18;
}
function totalIdle() external view returns (uint256) {
return 0;
}
function totalDebt() external view returns (uint256) {
return 0;
}
function isShutdown() external view returns (bool) {
return false;
}
}
Add this POC to LendingPool.test.js in describe("Borrow and Repay", section:
/test/unit/core/pools/LendingPool/LendingPool.test.js:356
356: it.only("POC: No assets will be deposit on crvVault",async function(){
357: const borrowAmount = ethers.parseEther("100");
358: const MockVault = await ethers.getContractFactory("MockUsdVault");
359: let vault = await MockVault.deploy(crvusd.target);
360:
361:
362: await lendingPool.connect(owner).setCurveVault(vault.target);
363: const depositAmount = ethers.parseEther("1000");
364: await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
365: await lendingPool.connect(user2).deposit(depositAmount);
366:
367: })
Run the command npx hardhat test
Impact
LendingPool cannot deposit assets into curveVault, leading to liquidity mismanagement.
Tools Used
Manual Review, Unit Testing
Recommendations
Modify _depositIntoVault to transfer assets from the RToken to LendingPool and then perform deposit to curveVault.
/contracts/core/pools/LendingPool/LendingPool.sol:858
function _depositIntoVault(uint256 amount) internal {
+ IRToken(reserve.reserveRTokenAddress).transferAsset(address(this), amount);
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);