Summary
Wrong withdraw funds in LendingPool#_withdrawFromVault() cause transaction reverts and prevent funds from being accessible for withdrawals, disrupting protocol functionality.
Vulnerability Details
The _withdrawFromVault() is designed to withdraw funds from the Curve vault when the reserve lacks enough funds.
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
...
@> _ensureLiquidity(amount);
...
}
function _ensureLiquidity(uint256 amount) internal {
...
if (availableLiquidity < amount) {
uint256 requiredAmount = amount - availableLiquidity;
@> _withdrawFromVault(requiredAmount);
}
}
However, this function sends the withdrawn funds to this (LendingPool) instead of reserve.reserveRTokenAddress, which actually holds user liquidity. Additionally, the function incorrectly specifies msg.sender as the vault share owner instead of this, even though the Curve vault shares are owned by the LendingPool contract (since _depositIntoVault() deposits funds from this contract).
curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
Proof Of Code
Make MockCurveCrvUSDVault
pragma solidity ^0.8.19;
import "../interfaces/curve/ICurveCrvUSDVault.sol";
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MockCurveCrvUSDVault is ERC20 {
address asset;
constructor(address _asset) ERC20("Mock curveCrvUSD", "curveCrvUSD") {
asset = _asset;
}
function deposit(uint256 assets, address receiver) external returns (uint256 shares) {
IERC20(asset).transferFrom(msg.sender, address(this), assets);
_mint(receiver, assets);
shares = assets;
}
function withdraw(
uint256 assets,
address receiver,
address owner,
uint256 maxLoss,
address[] calldata strategies
) external returns (uint256 shares) {
IERC20(asset).transfer(receiver, assets);
_burn(owner, assets);
shares = assets;
}
}
Update LendingPool.test.js
describe("LendingPool", function () {
...
beforeEach(async function () {
...
const LendingPool = await ethers.getContractFactory("LendingPool");
lendingPool = await LendingPool.deploy(
crvusd.target,
rToken.target,
debtToken.target,
raacNFT.target,
raacHousePrices.target,
initialPrimeRate
);
+ const scrvCurve = await ethers.getContractFactory("MockCurveCrvUSDVault");
+ const curveVault = await scrvCurve.deploy(crvusd.target);
+ await lendingPool.connect(owner).setCurveVault(curveVault);
...
+ const depositAmount = ethers.parseEther("1000");
+ await crvusd.connect(user2).approve(lendingPool.target, depositAmount);
+ await lendingPool.connect(user2).deposit(depositAmount);+
+ const withdrawAmount = ethers.parseEther("800");
+ await lendingPool.connect(user2).withdraw(withdrawAmount);
});
});
As seen above, withdraw transaction reverts in _withdrawFromVault().
Impact
Core function - withdraw and borrow reverts
Tools Used
manual
Recommendations
- curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
+ curveVault.withdraw(amount, reserve.reserveRTokenAddress, address(this), 0, new address[](0));