20,000 USDC
View results
Submission Details
Severity: high

Borrower worse off when auctioned loan is bought by pool with higher interest rate

Summary

A borrower will become worse off when their auctioned loan is bought by a pool with a higher interest rate.

Vulnerability Details

The relevant contest details read: Lenders can give away their loan at any point so long as, the pool they are giving it to offers same or better lending terms...When a lender no longer wants to be in a loan, but there is no lending pool available to give the loan to, lenders are able to put the loan up for auction.... Anyone is able to match an active pool with a live auction when the parameters of that pool match that of the auction or are more favorable to the borrower.

The idea conveyed here is that if a lender doesn't want a loan anymore in their pool, they can give the loan to another pool either directly or via auction, as long as the other pool has the same or better lending terms. The most important lending term is the interest rate, and Lender.buyLoan() never checks if the buying pool's interest rate is the same or lower than the old pool's. This means that an auctioned loan can be bought into a pool with a higher interest rate where the borrower will be worse off.

Proof of concept, first add these utility functions to Lender.sol:

// added for audit contest proof of concept
function getLoanLender(uint256 loanId) external view returns (address lender) {
return loans[loanId].lender;
}
// added for audit contest proof of concept
function getLoanInterestRate(uint256 loanId) external view returns (uint256 interestRate) {
return loans[loanId].interestRate;
}

Then add this test to test/Lender.t.sol:

function test_buyLoanToHigherInterestRate() public {
// interest rate of first pool created in test_borrow()
uint FIRST_POOL_INTEREST_RATE = 1000;
// second pool created in this test has a higher interest rate
// so the borrower will be worse off
uint SECOND_POOL_INTEREST_RATE = 2000;
// using modified setup from test_buyLoan()
test_borrow();
// accrue interest
vm.warp(block.timestamp + 364 days + 12 hours);
// kick off auction
vm.startPrank(lender1);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
// loan is owned by lender1
assertEq(lender1, lender.getLoanLender(loanIds[0]));
// loan has interest rate of first pool
assertEq(FIRST_POOL_INTEREST_RATE, lender.getLoanInterestRate(loanIds[0]));
lender.startAuction(loanIds);
vm.startPrank(lender2);
Pool memory p = Pool({
lender: lender2,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1 days,
interestRate: SECOND_POOL_INTEREST_RATE, // second pool has higher rate
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
lender.buyLoan(0, poolId);
// loan is now owned by lender2
assertEq(lender2, lender.getLoanLender(loanIds[0]));
// loan now has interest rate of second pool
assertEq(SECOND_POOL_INTEREST_RATE, lender.getLoanInterestRate(loanIds[0]));
// interest rate of second pool is greater than first pool,
// borrower is worse off
assert(SECOND_POOL_INTEREST_RATE > FIRST_POOL_INTEREST_RATE);
}

Impact

Borrower will be worse off as they will have a higher interest rate, breaking the written system requirements.

Tools Used

Manual

Recommendations

Lender.buyLoan() must ensure that the buying pool's interest rate is equal to or lower than the old pool's interest rate.

Support

FAQs

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