code-423n4 / 2024-06-size-findings

3 stars 1 forks source link

Credit can be sold forcibly as `forSale` setting can be ignored via Compensate #179

Open howlbot-integration[bot] opened 4 months ago

howlbot-integration[bot] commented 4 months ago

Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/AccountingLibrary.sol#L62-L82 https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/Compensate.sol#L118-L145

Vulnerability details

Any credit position forSale set by the lender can be overridden to be true by their borrower with Compensate.

Impact

Any credit position can be forcibly sold with the help of its borrower. This will be executed only when have expected profit, for example, when lender's curve is not null and is above the market it is profitable to buy credit from them (lend to them at above market rates), but they might block it with the lack of free collateral and the forSale = false flag, which can be overridden by the corresponding borrower of this credit position. The impact of this forced sale is proportional to interest rate volatility and can be substantial. There are no additional prerequisites for the setup.

Proof of Concept

createDebtAndCreditPositions creates credit positions with forSale == true:

AccountingLibrary.sol#L62-L82

    function createDebtAndCreditPositions(
        ...
    ) external returns (CreditPosition memory creditPosition) {
        DebtPosition memory debtPosition =
            DebtPosition({borrower: borrower, futureValue: futureValue, dueDate: dueDate, liquidityIndexAtRepayment: 0});

        uint256 debtPositionId = state.data.nextDebtPositionId++;
        state.data.debtPositions[debtPositionId] = debtPosition;

        emit Events.CreateDebtPosition(debtPositionId, borrower, lender, futureValue, dueDate);

        creditPosition = CreditPosition({
            lender: lender,
            credit: debtPosition.futureValue,
            debtPositionId: debtPositionId,
>>          forSale: true
        });

This can be done on demand for any credit position by the borrower of the corresponding debt position, by running Compensate with params.creditPositionToCompensateId == RESERVED_ID, which will create new position with forSale == true and substitute the existing lender position with it:

Compensate.sol#L118-L145

        CreditPosition memory creditPositionToCompensate;
        if (params.creditPositionToCompensateId == RESERVED_ID) {
>>          creditPositionToCompensate = state.createDebtAndCreditPositions({  // @audit create new credit with `forSale == true`
                lender: msg.sender,
                borrower: msg.sender,
                futureValue: amountToCompensate,
                dueDate: debtPositionToRepay.dueDate
            });
        } else {
            creditPositionToCompensate = state.getCreditPosition(params.creditPositionToCompensateId);
            amountToCompensate = Math.min(amountToCompensate, creditPositionToCompensate.credit);
        }

        // debt and credit reduction
        state.reduceDebtAndCredit(
            creditPositionWithDebtToRepay.debtPositionId, params.creditPositionWithDebtToRepayId, amountToCompensate
        );

        uint256 exiterCreditRemaining = creditPositionToCompensate.credit - amountToCompensate;

        // credit emission
>>      state.createCreditPosition({
            exitCreditPositionId: params.creditPositionToCompensateId == RESERVED_ID  // @audit give it to the lender instead of the existing position with `forSale == false`
                ? state.data.nextCreditPositionId - 1
                : params.creditPositionToCompensateId,
            lender: creditPositionWithDebtToRepay.lender,
            credit: amountToCompensate
        });

Then it can be then bought with BuyCreditMarket:

BuyCreditMarket.sol#L79-L8

            borrower = creditPosition.lender;
            tenor = debtPosition.dueDate - block.timestamp; // positive since the credit position is transferrable, so the loan must be ACTIVE
        }

        BorrowOffer memory borrowOffer = state.data.users[borrower].borrowOffer;

Tools Used

Manual Review

Recommended Mitigation Steps

Consider passing the flag to createDebtAndCreditPositions() indicating forSale flag to be set, which be passed from the existing credit in Compensate.sol#L139, so it won't be changed.

Assessed type

Other

aviggiano commented 4 months ago

Fixed in https://github.com/SizeCredit/size-solidity/pull/130

hansfriese commented 4 months ago

Valid finding.

Medium is more appropriate due to the below reasons.

c4-judge commented 4 months ago

hansfriese marked the issue as satisfactory

c4-judge commented 4 months ago

hansfriese changed the severity to 2 (Med Risk)

c4-judge commented 4 months ago

hansfriese marked the issue as selected for report