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

Buying a loan always reverts at the start of an auction

Summary

When the auction of a loan is at the its beginning no one can buy the loan because of the currentAuctionRate.

Vulnerability Details

Current auction rate is calculated wrong in the buyLoan function. This prevents the users from buying a loan that is put for an auction.
Let's say that Alice (a lender) puts Bob's (borrower) loan for an auction. Alice set the auction length to be 2 days which means that the loan's auctionLength would be 2 days as well (it is set in the borrow function). Now let's imagine that a third person (we will call him Andy) wants to buy the loan as soon as the auction starts so after the startAuction function is called by Alice, Andy call buyLoan function to buy the loan in his already created pool. Andy won't be able to buy the loan because of the calculations inside the buyLoan function:

uint256 currentAuctionRate = (MAX_INTEREST_RATE * timeElapsed) /
        loan.auctionLength; //@ audit rate is too high
    // validate the rate
    if (pools[poolId].interestRate > currentAuctionRate) revert RateTooHigh();

Let's see what the value of currentAuctionRate will be. The MAX_INTEREST_RATE = 100,000 and if timeElapsed is lower than loan.autionLength / 100 the function will revert with RateTooHigh exception. This means that 1/100 of the loan.autionLength should have passed in order for the loan to be bought.

Proof Of Concept

You need to add this uint256[] loanIds; and this Borrow[] borrowArray; as a storage variables in the test to work properly.
Add this test in the Lender.t.sol to see what is happening:

function testBuyLoan() 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: 2 days,
        interestRate: 1000,
        outstandingLoans: 0
    });
    lender.setPool(p);
    vm.stopPrank();
    
    vm.startPrank(lender2);
    Pool memory p1 = Pool({
        lender: lender2,
        loanToken: address(loanToken),
        collateralToken: address(collateralToken),
        minLoanSize: 100*10**18,
        poolBalance: 1000*10**18,
        maxLoanRatio: 2*10**18,
        auctionLength: 1,
        interestRate: 1000,
        outstandingLoans: 0
    });
    lender.setPool(p1);
    vm.stopPrank();

    bytes32 poolId = lender.getPoolId(lender1, address(loanToken), address(collateralToken));
    bytes32 poolId2 = lender.getPoolId(lender2, address(loanToken), address(collateralToken));

    vm.startPrank(borrower);
    Borrow memory borrow = Borrow({
        poolId: poolId,
        debt: 200e18,
        collateral: 100e18
    });
    
    borrowArray.push(borrow);
    lender.borrow(borrowArray);
    vm.stopPrank();

    loanIds.push(0);

    vm.startPrank(lender1);
    lender.startAuction(loanIds);
    vm.stopPrank();

    vm.warp(block.timestamp + (2 days / 100) - 1);

    vm.startPrank(borrower);
    lender.buyLoan(0, poolId2);
    vm.stopPrank();

    (,,,,uint256 poolBalance,,,,) = lender.pools(poolId);
    (,,,,uint256 poolBalance2,,,,) = lender.pools(poolId2);

    console.log("Pool 1 balance: ", poolBalance);
    console.log("Pool 2 balanace: ", poolBalance2);
}

In this test we try to buy the loan to another pool but we encounter a RateTooHigh exception. If we move the block.timestamp by one more second then we will be able to buy the loan and the transaction will succeed.

Impact

This leads to the buyLoan function being blocked at the beginning of an auction.

Tools Used

Visual Studio Code, Foundry

Recommendations

Instead of doing this calculations just check if the pools[poolId].interestRate > MAX_INTEREST_RATE.

Support

FAQs

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