Skip to content

Latest commit

 

History

History
106 lines (78 loc) · 3.92 KB

106.md

File metadata and controls

106 lines (78 loc) · 3.92 KB

Stable Midnight Canary

High

Borrower can borrow & repay in the same transaction making the lender pay fee without locking collateral

Summary

Current implementation allows borrowers to borrow ERC20 tokens and repay the taken loan in the same transaction. This way the debt payed will be the same as the amount taken from the lender and this way borrower will have to pay only gas fees which are negligible in L2 chains. On the other side, the fee payed for the lending operation is payed by the lender. This provides a low cost attack vector for malicious actor to drain lenders funds.

Root Cause

Root cause lies in the fact that borrowers can repay the loan in the same transaction without paying some interest for it and in the same time lenders are the one who pay the fee for lending in acceptLoanOffer() scenario.

Internal pre-conditions

There need to be an active loan offer proposal, which is very likely as this is the whole idea of the protocol.

External pre-conditions

No specific external pre-conditions.

Attack Path

In the same transaction user calls acceptLoanOffer() and then repays it.

Impact

Lender funds will be drained and the protocol will become unusable.

PoC

acceptOffer(): https://github.com/sherlock-audit/2024-09-predict-fun/blob/41e70f9eed3f00dd29aba4038544150f5b35dccb/predict-dot-loan/contracts/PredictDotLoan.sol#L976 repay(): https://github.com/sherlock-audit/2024-09-predict-fun/blob/41e70f9eed3f00dd29aba4038544150f5b35dccb/predict-dot-loan/contracts/PredictDotLoan.sol#L454 pow(): https://github.com/sherlock-audit/2024-09-predict-fun/blob/41e70f9eed3f00dd29aba4038544150f5b35dccb/predict-dot-loan/contracts/libraries/InterestLib.sol#L17

fee is payed by the lender: uint256 protocolFee = _transferLoanAmountAndProtocolFee(lender, borrower, fulfillAmount);

    function _transferLoanAmountAndProtocolFee(
        address from,
        address to,
        uint256 loanAmount
    ) private returns (uint256 protocolFee) {
        protocolFee = (loanAmount * protocolFeeBasisPoints) / 10_000;
        LOAN_TOKEN.safeTransferFrom(from, to, loanAmount - protocolFee);
        if (protocolFee > 0) {
            LOAN_TOKEN.safeTransferFrom(from, protocolFeeRecipient, protocolFee);
        }
    }

As it can be seen from the following snippets, the debt calculated will be equal to the loanAmount as the time elapsed will be zero.

    function repay(uint256 loanId) external nonReentrant {
        Loan storage loan = loans[loanId];

        _assertAuthorizedCaller(loan.borrower);

        LoanStatus status = loan.status;
        if (status != LoanStatus.Active) {
            if (status != LoanStatus.Called) {
                revert InvalidLoanStatus();
            }
        }

        uint256 debt = _calculateDebt(loan.loanAmount, loan.interestRatePerSecond, _calculateLoanTimeElapsed(loan));

        loan.status = LoanStatus.Repaid;

        LOAN_TOKEN.safeTransferFrom(msg.sender, loan.lender, debt);
        CTF.safeTransferFrom(address(this), msg.sender, loan.positionId, loan.collateralAmount, "");

        emit LoanRepaid(loanId, debt);
    }
    function _calculateDebt(
        uint256 loanAmount,
        uint256 interestRatePerSecond,
        uint256 timeElapsed
    ) private pure returns (uint256 debt) {
        debt = (loanAmount * interestRatePerSecond.pow(timeElapsed)) / InterestLib.ONE;
    }
    function pow(uint256 _base, uint256 _exponent) public pure returns (uint256) {
        if (_exponent == 0) {
            return ONE;
        } else if (_exponent % 2 == 0) {
            uint256 half = pow(_base, _exponent / 2);
            return half * half / ONE;
        } else {
            return _base * pow(_base, _exponent - 1) / ONE;
        }
    }

Mitigation

Consider introducing a state variable which saves the block.timestamp where last borrower last borrowed lends and forbid repaying it in the same block.timestamp.