sherlock-audit / 2024-09-predict-fun-judging

5 stars 4 forks source link

Ikigai - Malicious user can perpetually DoS AUTO-REFINANCER's refinance function #277

Open sherlock-admin3 opened 1 month ago

sherlock-admin3 commented 1 month ago

Ikigai

Medium

Malicious user can perpetually DoS AUTO-REFINANCER's refinance function

Summary

The ability for borrowers to toggle auto-refinancing at any time will cause a perpetual Denial of Service (DoS) for the AUTO-REFINANCER. A malicious borrower can frontrun the refinance transaction by disabling their auto-refinancing, causing the entire transaction to revert, and then backrun by re-enabling it, allowing them to be included in future batches and repeat the attack.

Root Cause

In PredictDotLoan.sol, there are two key design choices that contribute to this vulnerability:

This "all-or-nothing" approach in processing the refinancings array makes the function vulnerable to a single malicious actor, allowing them to disrupt the entire batch operation. These two factors combined create a scenario where a malicious user can repeatedly cause the entire refinancing operation to fail, leading to a persistent DoS condition.

Internal pre-conditions

No response

External pre-conditions

No response

Attack Path

  1. A malicious user creates a LoanOffer proposal from Address A with artificially long duration and low loanAmount, making it unattractive to legitimate users.

  2. The same malicious user, using Address B, accepts this LoanOffer as a Borrower.

  3. Using Address B (Borrower), the malicious user toggles auto-refinancing on, making this loan eligible for the AUTO-REFINANCER bot to seek better terms.

  4. The malicious user, using Address C, creates another LoanOffer that is more attractive and suitable for auto-refinancing based on the terms of the loan held by Address B (but still unattractive to legitimate users)

  5. The AUTO-REFINANCER bot identifies this new LoanOffer as a better option and includes it in a batch refinance([]) call.

  6. Just before the AUTO-REFINANCER's transaction is processed, the malicious user (using Address B) frontruns by calling toggleAutoRefinancingEnabled() to disable auto-refinancing.

  7. The AUTO-REFINANCER's refinance([]) transaction executes and reverts when processing the refinancing for Address B due to the check [Github link]:

    if (autoRefinancingEnabled[borrower] == 0) {
        revert BorrowerDidNotEnableAutoRefinancing(borrower);
    }
  8. The entire refinance operation fails, affecting all borrowers in the batch.

  9. The malicious user (Address B) backruns the failed transaction by calling toggleAutoRefinancingEnabled() again to re-enable auto-refinancing.

  10. The loan associated with Address B is now again eligible to be included in future refinancing batches.

  11. Steps 5-10 can be repeated indefinitely, causing a perpetual DoS on the AUTO-REFINANCER's refinance function.

This revised attack path demonstrates how a malicious user can set up the conditions for the attack using multiple addresses and manipulate the system to repeatedly disrupt the auto-refinancing process.

Impact

The AUTO-REFINANCER suffers from a persistent Denial of Service, unable to execute batch refinancing operations. This affects all borrowers included in the refinancing batches, potentially causing them to miss out on better loan terms.

The Protocol team incurs losses due to wasted gas fees on repeatedly failed transactions, while the malicious user's costs remain minimal.

PoC

No response

Mitigation

Move the check for autoRefinancingEnabled earlier in the refinancing loop and use a continue statement to skip borrowers who have disabled auto-refinancing:

function refinance(Refinancing[] calldata refinancings) external nonReentrant whenNotPaused onlyRole(REFINANCIER_ROLE) {
    RefinancingResult[] memory results = new RefinancingResult[](refinancings.length);
    for (uint256 i; i < refinancings.length; ++i) {
        Refinancing calldata refinancing = refinancings[i];
        Loan storage loan = loans[refinancing.loanId];
        address borrower = loan.borrower;

        if (autoRefinancingEnabled[borrower] == 0) {
            continue; // Skip this refinancing if auto-refinancing is disabled
        }

        (uint256 id, Loan memory newLoan, uint256 protocolFee) = _refinance(refinancing);

        // ... rest of the function
    }
    emit LoansRefinanced(results);
}

This change allows the function to skip borrowers who have disabled auto-refinancing without causing the entire transaction to revert, ensuring that other borrowers in the batch can still benefit from refinancing.