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

0 stars 0 forks source link

Hot Carrot Jaguar - A malicious user can create empty loans with the loan offer which is fully fulfilled #275

Open sherlock-admin4 opened 2 days ago

sherlock-admin4 commented 2 days ago

Hot Carrot Jaguar

Medium

A malicious user can create empty loans with the loan offer which is fully fulfilled

Summary

The miss checking in a fully fulfilled loan allows a malicious user to create limitless empty loans, whose lender is a normal user. These empty loans can spam the frontend protocol if mishandled and mislead the unconscious lender.

Root Cause

In validation logic, it only includes checks _assertFulfillAmountNotTooLow() and _assertFulfillAmountNotTooHigh() but not check whether the loan is already fully fulfilled.

https://github.com/sherlock-audit/2024-09-predict-fun/blob/main/predict-dot-loan/contracts/PredictDotLoan.sol#L1269-L1289

Zero fulfillAmount value can pass the check when a loan is fully fulfilled and used to create empty loans.

Internal pre-conditions

Any valid and fully fulfilled loans.

External pre-conditions

The frontend protocol has not taken this kind of spam attack into consideration.

Attack Path

The malicious user calls acceptLoanOffer() with zero fulfillment and just needs to pay gas fee.

Impact

The protocal will suffer from spam if empty loans are mishandled frontend. The LoanStatus of an empty loan is Active unlike others with zero debt. It can mislead the lender of empty loans to try call() and seize() but get nothing.

PoC

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.25;

import {IPredictDotLoan} from "../../contracts/interfaces/IPredictDotLoan.sol";

import {PredictDotLoan_Test} from "./PredictDotLoan.t.sol";
import {MockEIP1271Wallet} from "../mock/MockEIP1271Wallet.sol";
import {MockUmaCtfAdapter} from "../mock/MockUmaCtfAdapter.sol";

contract PredictDotLoan_Bug_Test is PredictDotLoan_Test {
    function test_Bug() public {
        wallet = new MockEIP1271Wallet(lender);
        vm.label(address(wallet), "Lender's EIP-1271 Wallet");
        mockERC20.mint(address(wallet), LOAN_AMOUNT);
        vm.prank(address(wallet));
        mockERC20.approve(address(predictDotLoan), LOAN_AMOUNT);
        IPredictDotLoan.Proposal memory proposal = _generateLoanOffer(IPredictDotLoan.QuestionType.Binary);
        proposal.from = address(wallet);
        proposal.signature = _signProposal(proposal);
        _assertBalanceAndFulfillmentBeforeExecution(borrower, lender, proposal);
        _assertProposalAcceptedEmitted(predictDotLoan.hashProposal(proposal), borrower, address(wallet));
        vm.prank(borrower);
        predictDotLoan.acceptLoanOffer(proposal, proposal.loanAmount);

        // A malicous user "borrower2" with even no loan or CTF tokens.
        vm.startPrank(borrower2);
        mockCTF.setApprovalForAll(address(predictDotLoan), true);
        predictDotLoan.acceptLoanOffer(proposal, 0);
        vm.stopPrank();
    }
}

Mitigation

Check whether a loan is fully fulfilled in the validation logic:

    function _assertFulfillAmountNotTooHigh(
        uint256 fulfillAmount,
        uint256 fulfilledAmount,
        uint256 loanAmount
    ) private pure {
-       if (fulfilledAmount + fulfillAmount > loanAmount) {
+       if (fulfilledAmount + fulfillAmount > loanAmount || fulfilledAmount == loanAmount) {
            revert FulfillAmountTooHigh();
        }
    }