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 {
bytes32 poolId;
uint256 debt;
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:
Borrower calls borrow() function. He specify a pool which has an interest of 0.1%.
Malicious lender see the transaction and frontrun it, the changes the pool interest rate to 0.2%.
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:
function test_lender_can_change_interestRate_at_anytime() public {
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);
(,,,,,,,uint256 interestRate,) = lender.pools(poolId);
assertEq(interestRate, 1000);
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,
outstandingLoans: 0
});
lender.setPool(p);
(,,,,,,,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();