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