Summary
The _depositIntoVault function in the LendingPool contract attempts to deposit assets into Curve vault without first transferring them from the reserveRTokenAddress. The assets need to be transferred to the lending pool contract using RToken.transferAsset() before depositing into the Curve vault.
Vulnerability Details
function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}
The issue occurs because:
Assets are held by reserveRTokenAddress (RToken contract)
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
);
return amountMinted;
}
LendingPool tries to deposit directly to Curve vault without first obtaining the assets
The approve() and deposit() calls will fail due to insufficient balance
Impact
Rebalancing operations will fail causing DOS
Asset locked in the vault when curve vault is not immediately added after deployment
Tools Used
Manual/Hardhat
##POC
Mock Curve vault
contract MockCurveVault is Ownable {
IERC20 public reserveAsset;
mapping(address => uint256) public balances;
event Deposit(address indexed user, uint256 amount);
event Withdraw(address indexed user, uint256 amount);
constructor(address _reserveAsset) Ownable(msg.sender){
reserveAsset = IERC20(_reserveAsset);
}
function deposit(uint256 amount, address recipient) external {
require(amount > 0, "Amount must be greater than 0");
reserveAsset.transferFrom(msg.sender, address(this), amount);
balances[recipient] += amount;
emit Deposit(recipient, amount);
}
function withdraw(uint256 amount, address to, address from, uint256, address[] calldata) external {
require(amount > 0, "Amount must be greater than 0");
require(balances[from] >= amount, "Insufficient balance");
balances[from] -= amount;
reserveAsset.transfer(to, amount);
emit Withdraw(from, amount);
}
function getBalance(address user) external view returns (uint256) {
return balances[user];
}
}
In LendingPool.test.js
beforeEach(async function () {
[owner, user1, user2, user3, attacker, innocentUser] = await ethers.getSigners();
const CurveVault = await ethers.getContractFactory("MockCurveVault");
curveVault = await CurveVault.deploy(crvusd.target);
const LendingPool = await ethers.getContractFactory("LendingPool");
lendingPool = await LendingPool.deploy(
crvusd.target,
rToken.target,
debtToken.target,
raacNFT.target,
raacHousePrices.target,
initialPrimeRate
);
});
describe("Deposit fails", function () {
it("should revert when depositing in curve without owning asset", async function () {
const depositAmount = ethers.parseEther("100");
await lendingPool.setCurveVault(curveVault.target);
await expect(lendingPool.connect(user2).deposit(depositAmount)).to.be.reverted;
});
});
Recommendations
Implement proper asset transfer sequence:
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;
}