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

Malicious lender can frontrun `borrow()` function and make borrowers to pay more interests

Summary

The malicious lender can frontrun the borrow() function and make borrower to pay more interests.

Vulnerability Details

The borrow() function helps to borrow money from a specific pool. The borrower specify the the next parameters:

struct Borrow {
/// @notice the pool ID to borrow from
bytes32 poolId;
/// @notice the amount to borrow
uint256 debt;
/// @notice the amount of collateral to put up
uint256 collateral;
}

The problem is that the borrower transaction can be frontrunned by a malicious lender and make him to pay more interests. Please see the next scenario:

  1. Borrower calls borrow() function. He specify a pool which has an interest of 0.1%.

  2. Malicious lender see the transaction and frontrun it, the changes the pool interest rate to 0.2%.

  3. After the step 2, the borrow action is executed onchain and the borrower acquired a loan with 0.2% interest rate.

I created a test where the pool interest rate can be changed at any time using the setPool() function:

// File: test/Lender.t.sol:LenderTest
// $ forge test --match-test "test_lender_can_change_interestRate_at_anytime" -vvv
//
function test_lender_can_change_interestRate_at_anytime() public {
// Malicious lender can frontrun the borrower and make him to pay more interests
// 1. Lender1 creates the pool with 1000 BIPs interest rate
// 2. Assert interestRate at 1000 BIPs
// 3. Lender1 changes the interest rate to 2000
// 4. Assert interestRate at 2000 BIPs
//
// 1. Lender1 creates the pool with 1000 BIPs interest rate
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 1 * 10**18,
poolBalance: 1000 * 10**18,
maxLoanRatio: 2 * 10**18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
//
// 2. Assert interestRate at 1000 BIPs
(,,,,,,,uint256 interestRate,) = lender.pools(poolId);
assertEq(interestRate, 1000);
//
// 3. Lender1 changes the interest rate to 2000
p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 1 * 10**18,
poolBalance: 2000 * 10**18,
maxLoanRatio: 2 * 10**18,
auctionLength: 1 days,
interestRate: 2000, // interest rate to 2000
outstandingLoans: 0
});
lender.setPool(p);
//
// 4. Assert interestRate at 2000 BIPs
(,,,,,,,interestRate,) = lender.pools(poolId);
assertEq(interestRate, 2000);
}

Impact

Malicious lender can make the borrower to accept loans with high interests. The borrower can repay() the debt and choose another pool but he needs to pay fees in the borrow action and in the repay action so at least the borrower will lost some tokens.

Tools used

Manual review

Recommendations

Allow the borrower to specify the interest is allowed to accept:

struct Borrow {
/// @notice the pool ID to borrow from
bytes32 poolId;
/// @notice the amount to borrow
uint256 debt;
/// @notice the amount of collateral to put up
uint256 collateral;
++ /// @noticie the interest the borrower is allowed to accept
++ uint256 interestRate;
}
function borrow(Borrow[] calldata borrows) public {
for (uint256 i = 0; i < borrows.length; i++) {
bytes32 poolId = borrows[i].poolId;
uint256 debt = borrows[i].debt;
uint256 collateral = borrows[i].collateral;
++ uint256 borrowerInterestRate = borrows[i].interestRate
// get the pool info
Pool memory pool = pools[poolId];
++ if (borrowerInterestRate != pool.interestRate) revert();

Support

FAQs

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