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

5 stars 4 forks source link

0xAadi - Static Collateral Checks in `matchProposals()` Prevent Borrowers from Utilizing Updated/Improved Collateralization Ratios After a Partial Fulfillment #313

Open sherlock-admin3 opened 1 month ago

sherlock-admin3 commented 1 month ago

0xAadi

High

Static Collateral Checks in matchProposals() Prevent Borrowers from Utilizing Updated/Improved Collateralization Ratios After a Partial Fulfillment

Summary

The use of static collateralization ratio checks in matchProposals() will cause unnecessary restrictions for borrowers as the contract fails to account for the dynamic nature of the collateralization of partially fulfilled matches, potentially blocking valid matches with suitable loan offers.

Root Cause

Unlike other loan creation methods, matchProposals() has the ability to improve the collateralization ratio of a borrow request. This is due to the unique way in which matchProposals() operates.

The collateral amount required is calculated based on the loan offer, and this amount is used to update the fulfillment of the borrow request.

            collateralAmountRequired = _calculateCollateralAmountRequired(
                loanOffer,
                loanOfferFulfillment,
                fulfillAmount
            );

            _updateFulfillment(
                borrowRequestFulfillment,
                collateralAmountRequired,
                fulfillAmount,
                borrowRequestProposalId
            );

https://github.com/sherlock-audit/2024-09-predict-fun/blob/41e70f9eed3f00dd29aba4038544150f5b35dccb/predict-dot-loan/contracts/PredictDotLoan.sol#L395C1-L406C15

Since the invariant requires that the borrow request's collateralization ratio must be higher than the loan offer's collateralization ratio, the mechanism described above will positively update the collateralization ratio of the borrow request in the case of a partial match.

        if (
            borrowRequest.collateralAmount * loanOffer.loanAmount <
            borrowRequest.loanAmount * loanOffer.collateralAmount
        ) {
            revert UnacceptableCollateralizationRatio();
        }

https://github.com/sherlock-audit/2024-09-predict-fun/blob/41e70f9eed3f00dd29aba4038544150f5b35dccb/predict-dot-loan/contracts/PredictDotLoan.sol#L351C9-L357C1

The vulnerability is that, even if the collateralization ratio has improved, the check still uses the amounts in the borrow request to calculate the ratio, which may be outdated.

Internal pre-conditions

No response

External pre-conditions

  1. Sign two matchable proposals by offchain

Example proposals

Proposal borrowRequest

Proposal({
    from: 0xBorrowerAddress,
    loanAmount: 1000 ether,
    collateralAmount: 1500 ether, // 150% collateral ratio
    questionType: QuestionType.Binary,
    questionId: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef,
    outcome: true,
    interestRatePerSecond: 1.00000001 ether,
    duration: 30 days,
    validUntil: block.timestamp + 7 days,
    salt: 123456,
    nonce: 1,
    proposalType: ProposalType.BorrowRequest,
    signature: 0xSignatureBorrowRequest,
    protocolFeeBasisPoints: 50
});

Proposal loanOffer

Proposal({
    from: 0xLenderAddress,
    loanAmount: 500 ether,
    collateralAmount: 500 ether, // 100% collateral ratio
    questionType: QuestionType.Binary,
    questionId: 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef,
    outcome: true,
    interestRatePerSecond: 1.00000001 ether,
    duration: 60 days,
    validUntil: block.timestamp + 7 days,
    salt: 654321,
    nonce: 1,
    proposalType: ProposalType
    signature: 0xSignatureBorrowRequest,
    protocolFeeBasisPoints: 50
});

Attack Path

  1. The above proposals have the following collateralization ratios: 150% for the borrow request and 100% for the loan offer.

For the borrow request:

collateralization ratio = collateralAmount / loanAmount = 1500 / 1000 = 150%
  1. When matching the above proposals, a borrow fulfillment struct is created:
Fulfillment borrowRequestFulfillment = Fulfillment({
    proposalId: 0xBorrowRequestProposalId, // The hash of the borrow request proposal
    collateralAmount: 500 ether, // Collateral amount matched from the loan offer
    loanAmount: 500 ether // Loan amount matched from the loan offer
});

Here we can see that the borrow request has been fulfilled with a collateralAmount of 500 ether and a loanAmount of 500 ether.

This means the remaining borrow request still needs to fulfill a collateralAmount of 1000 ether and a loanAmount of 500 ether.

So the calculation for the remaining portion will be:

collateralization ratio = collateralAmount / loanAmount = 1000 / 500 = 200%
  1. When trying to match with another loan offer that has a 200% ratio, it will fail due to the use of outdated collateralAmount and loanAmount from the original borrow request to calculate the ratio.

Impact

Restricts Matches: Borrowers with partially fulfilled requests and higher effective collateral ratios may be unable to match with suitable loan offers.

PoC

No response

Mitigation

Dynamic Collateral Ratio Calculation: Adjust the logic to calculate the effective collateral ratio based on the remaining unfulfilled portion of the borrow request. This ensures that the check accurately reflects the borrower's current collateral position.

sherlock-admin2 commented 1 month ago

The protocol team fixed this issue in the following PRs/commits: https://github.com/PredictDotFun/predict-dot-loan/pull/32