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

0 stars 0 forks source link

Overt Fossilized Elephant - Borrower Receives Less Than Expected Loan Amount Due to Protocol Fee Deduction #311

Open sherlock-admin4 opened 2 days ago

sherlock-admin4 commented 2 days ago

Overt Fossilized Elephant

Medium

Borrower Receives Less Than Expected Loan Amount Due to Protocol Fee Deduction

Summary

In the acceptLoanOffer() and acceptBorrowOffer() functions, the _acceptOffer() internal function is responsible for handling the offer acceptance process. This function calls _transferLoanAmountAndProtocolFee(), which deducts the protocol fee from the loan amount before transferring the remaining amount to the borrower. This behavior results in the borrower receiving less than the expected loan amount, leading to an inaccurate fulfillment of the loan agreement.

Specifically, when Alice (the lender) accepts Bob’s (the borrower’s) loan offer, Bob receives a reduced amount due to the protocol fee deduction, though he is still required to provide the full collateral and repay the original loan amount. This creates an imbalance in the loan agreement, which could lead to repayment issues and borrower dissatisfaction.

Root Cause

The core issue lies in the _transferLoanAmountAndProtocolFee() function, where the protocol fee is deducted from the loan amount before being transferred to the borrower. The borrower is expected to receive the full loan amount but ends up receiving less due to the protocol fee deduction.

Here is the relevant code that highlights the problem:

function _transferLoanAmountAndProtocolFee(
    address lender,
    address borrower,
    uint256 loanAmount,
    uint256 protocolFeeBasisPoints
) internal {
    uint256 protocolFee = loanAmount * protocolFeeBasisPoints / 10000;
    uint256 amountToBorrower = loanAmount - protocolFee;

    // Transfer protocol fee
    token.transfer(protocolFeeRecipient, protocolFee);

    // Transfer remaining loan amount to borrower
    token.transfer(borrower, amountToBorrower);
}

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

Internal pre-conditions

  1. Admin need to set the ProtocolFeeBasisPoints greater than 0

External pre-conditions

No response

Attack Path

  1. Bob’s Loan Proposal:

    • Bob proposes a loan with the following parameters:
      • Loan amount: $500 \times 10^{18} \, \text{USDB}$
      • Collateral amount: $500 \times 10^{18} \, \text{CTF}$
      • Protocol fee: $1$ basis point ($0.01%$)
      • Interest rate per second: $1.000000003020262040 \times 10^{18}$
      • Loan duration: $86400$ seconds (1 day)
  2. Alice Accepts Bob’s Loan Offer:

    • Alice accepts Bob's proposal and transfers $500 \times 10^{18}$ USDB to the contract.
  3. Protocol Fee Calculation: The protocol fee is calculated as follows:

    $$\text{Protocol Fee} = \frac{500 \times 10^{18} \times 1}{10,000} = 5 \times 10^{18} \, \text{USDB}$$

  4. Incorrect Transfer to Bob:

    • Instead of transferring the full $500 \times 10^{18}$ USDB to Bob, the _transferLoanAmountAndProtocolFee() function deducts the protocol fee from the loan amount:

      $$\text{Amount Transferred to Bob} = 500 \times 10^{18} - 5 \times 10^{18} = 495 \times 10^{18} \, \text{USDB}$$

    • Bob only receives $495 \times 10^{18} \, \text{USDB}$, which is less than the agreed loan amount of $500 \times 10^{18} \, \text{USDB}$.

  5. Collateral Transfer:

    • Bob transfers the collateral of $500 \times 10^{18} \, \text{CTF}$ to the predictDotLoan contract.
  6. Loan Creation:

    • A loan is created where Bob owes Alice $500 \times 10^{18} \, \text{USDB}$ plus interest, but Bob only received $495 \times 10^{18} \, \text{USDB}$. This discrepancy could lead to repayment issues.

Impact

The borrower receives less than the intended loan amount due to the deduction of the protocol fee. This discrepancy could cause issues with loan repayment, as the borrower still needs to repay the full loan amount, despite receiving a reduced amount. In the worst-case scenario, this could lead to borrower default or disputes between the lender and borrower.

PoC

Commands to run the test:

# To run the following POC paste the below code in PredictDotLoan_AcceptBorrowRequest.t.sol file
# And run this command
forge test --mt test_acceptBorrowRequest_EIP1271_Bluedragon -vv

Add the following in the TesHelper.sol::setUp() function:

predictDotLoan.updateProtocolFeeBasisPoints(100);

Here is the POC code

   function test_acceptBorrowRequest_EIP1271_Bluedragon() public {
        wallet = new MockEIP1271Wallet(borrower);
        vm.label(address(wallet), "Borrower's EIP-1271 Wallet");
        _mintCTF(address(wallet));
        console.log(
            "CTF balance of wallet before loan is accepted: ",
            mockCTF.balanceOf(address(wallet), _getPositionId(true))
        );
        console.log("----------------------------------------------------");
        console.log("Balance of USDB before loan is accepted: ", mockERC20.balanceOf(address(wallet)));
        vm.prank(address(wallet));
        mockCTF.setApprovalForAll(address(predictDotLoan), true);
        IPredictDotLoan.Proposal memory proposal = _generateBorrowRequest(IPredictDotLoan.QuestionType.Binary);
        proposal.from = address(wallet);
        proposal.signature = _signProposal(proposal, borrowerPrivateKey);
        _assertBalanceAndFulfillmentBeforeExecution(address(wallet), lender, proposal);
        _assertProposalAcceptedEmitted(predictDotLoan.hashProposal(proposal), address(wallet), lender);
        vm.prank(lender);
        predictDotLoan.acceptBorrowRequest(proposal, proposal.loanAmount);
        //predictDotLoan.acceptBorrowRequest(proposal, 900e18);
        (,,uint256 fulfillmentAmount) = predictDotLoan.getFulfillment(proposal);
        console.log("----------------------------------------------------");
        console.log(
            "CTF balance of wallet after loan is accepted: ",
            mockCTF.balanceOf(address(wallet), _getPositionId(true))
        );
        console.log("----------------------------------------------------");
        console.log("Actual loan amount: ", fulfillmentAmount);
        console.log("----------------------------------------------------");
        console.log("Balance of USDB received: ", mockERC20.balanceOf(address(wallet)));
        assertEq(mockERC20.balanceOf(address(wallet)), proposal.loanAmount);
        assertEq(mockCTF.balanceOf(address(predictDotLoan), _getPositionId(true)), proposal.collateralAmount);
        (bytes32 proposalId, uint256 _collateralAmount, uint256 _loanAmount) = predictDotLoan.getFulfillment(proposal);
        assertEq(proposalId, predictDotLoan.hashProposal(proposal));
        assertEq(_collateralAmount, proposal.collateralAmount);
        assertEq(_loanAmount, proposal.loanAmount);
        (
            address _borrower,
            address _lender,
            uint256 positionId,
            uint256 collateralAmount,
            uint256 loanAmount,
            uint256 interestRatePerSecond,
            uint256 startTime,
            uint256 minimumDuration,
            uint256 callTime,
            IPredictDotLoan.LoanStatus status,
            IPredictDotLoan.QuestionType questionType
        ) = predictDotLoan.loans(1);
        assertEq(_borrower, address(wallet));
        assertEq(_lender, lender);
        assertEq(positionId, _getPositionId(true));
        assertEq(collateralAmount, COLLATERAL_AMOUNT);
        assertEq(loanAmount, LOAN_AMOUNT);
        assertEq(interestRatePerSecond, INTEREST_RATE_PER_SECOND);
        assertEq(startTime, vm.getBlockTimestamp());
        assertEq(minimumDuration, LOAN_DURATION);
        assertEq(callTime, 0);
        assertEq(uint8(status), uint8(IPredictDotLoan.LoanStatus.Active));
        assertEq(uint8(questionType), uint8(IPredictDotLoan.QuestionType.Binary));
   }

Here is the output of the test:

  Logs:
  CTF balance of wallet before loan is accepted:  1000000000000000000000
  ----------------------------------------------------
  Balance of USDB before loan is accepted:  0
  ----------------------------------------------------
  CTF balance of wallet after loan is accepted:  0
  ----------------------------------------------------
  Actual loan amount:  700000000000000000000
  ----------------------------------------------------
  Balance of USDB received:  693000000000000000000

Mitigation

To resolve this issue, the protocol fee should be charged separately from the loan amount so that the borrower receives the full loan amount.