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

3 stars 1 forks source link

LiquidateWithReplacement does not charge swap fees on the borrower #53

Open c4-bot-10 opened 5 months ago

c4-bot-10 commented 5 months ago

Lines of code

https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/LiquidateWithReplacement.sol#L146

Vulnerability details

Impact

When a user places a credit sell limit order, it may be either 1) bought by other users, 2) bought by the LiquidateWithReplacement action. However, for case 2), it does not charge a swap fee for the borrower, which contradicts to the swap fee definition.

Bug Description

Let's first see how LiquidateWithReplacement works. There are two steps:

  1. Liquidation: Liquidator sends the amount of debt debt.futureValue to the protocol, and receive collateral tokens as liquidation reward.
  2. Buy credit: Use the original lender's debt.futureValue as credit to buy credit from the borrower. This way the original lender can still get the same amount of credit at the same dueDate. The liquidator can then take away the difference between debt.futureValue and the amount sent to the borrower.

Note that step 2 is the same as buying credit using the BuyCreditMarket function.

However, according to the docs, all cash and credit swaps should charge swap fees on the borrower. The issue here is the swap in LiquidateWithReplacement does not charge swap fees.

Protocol fee; charged to the recipient of USDC on cash->credit and credit-> cash swaps. Prorated based on credit tenor (the fee is annualized).

    function executeLiquidateWithReplacement(State storage state, LiquidateWithReplacementParams calldata params)
        external
        returns (uint256 issuanceValue, uint256 liquidatorProfitCollateralToken, uint256 liquidatorProfitBorrowToken)
    {
        emit Events.LiquidateWithReplacement(params.debtPositionId, params.borrower, params.minimumCollateralProfit);

        DebtPosition storage debtPosition = state.getDebtPosition(params.debtPositionId);
        DebtPosition memory debtPositionCopy = debtPosition;
        BorrowOffer storage borrowOffer = state.data.users[params.borrower].borrowOffer;
        uint256 tenor = debtPositionCopy.dueDate - block.timestamp;

        liquidatorProfitCollateralToken = state.executeLiquidate(
            LiquidateParams({
                debtPositionId: params.debtPositionId,
                minimumCollateralProfit: params.minimumCollateralProfit
            })
        );

        uint256 ratePerTenor = borrowOffer.getRatePerTenor(
            VariablePoolBorrowRateParams({
                variablePoolBorrowRate: state.oracle.variablePoolBorrowRate,
                variablePoolBorrowRateUpdatedAt: state.oracle.variablePoolBorrowRateUpdatedAt,
                variablePoolBorrowRateStaleRateInterval: state.oracle.variablePoolBorrowRateStaleRateInterval
            }),
            tenor
        );
>       issuanceValue = Math.mulDivDown(debtPositionCopy.futureValue, PERCENT, PERCENT + ratePerTenor);
        liquidatorProfitBorrowToken = debtPositionCopy.futureValue - issuanceValue;

        debtPosition.borrower = params.borrower;
        debtPosition.futureValue = debtPositionCopy.futureValue;
        debtPosition.liquidityIndexAtRepayment = 0;

        emit Events.UpdateDebtPosition(
            params.debtPositionId,
            debtPosition.borrower,
            debtPosition.futureValue,
            debtPosition.liquidityIndexAtRepayment
        );

        state.data.debtToken.mint(params.borrower, debtPosition.futureValue);
>       state.data.borrowAToken.transferFrom(address(this), params.borrower, issuanceValue);
        state.data.borrowAToken.transferFrom(address(this), state.feeConfig.feeRecipient, liquidatorProfitBorrowToken);
    }

Proof of Concept

Say 2 users setup the same sell limit order.

First user's order was bought by a user via BuyCreditMarket, and second user's order was bought by LiquidateWithReplacement, but the swap fee is only charged to the first user. This is unfair and is not mentioned anywhere expected in the documents.

Tools Used

Manual review

Recommended Mitigation Steps

Also charge swap fees during executeLiquidateWithReplacement.

Assessed type

Other

aviggiano commented 4 months ago

This is the intended behavior. The reason why the new borrower does not pay the swap fee is that indeed there is no credit for cash swap. The fee was already paid by the first borrower.

Consider the following scenario:

  1. Alice borrows from Bob. Alice pays the swap fee
  2. There is a price change and now Alice is liquidatable
  3. The keeper bot calls liquidateWithReplacement and replaces Alice by Candy
  4. Candy does not pay the swap fee since there is no cash for credit swap. Alice already paid the fee in step 1
aviggiano commented 4 months ago

Feedback from the team:

hansfriese commented 4 months ago

Thanks for the detailed feedback. Will keep as a valid Medium.

c4-judge commented 4 months ago

hansfriese marked the issue as satisfactory

c4-judge commented 4 months ago

hansfriese marked the issue as selected for report