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

2 stars 0 forks source link

Users cannot eliminate the liquidation risk when the CR falls below the liquidation CR by repaying partially #78

Closed c4-bot-10 closed 3 months ago

c4-bot-10 commented 3 months ago

Lines of code

https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/libraries/actions/Compensate.sol#L120-L125 https://github.com/code-423n4/2024-06-size/blob/8850e25fb088898e9cf86f9be1c401ad155bea86/src/Size.sol#L250

Vulnerability details

Impact

When users' CR falls below the liquidation CR, their positions become liquidatable, and users will experience loss through liquidation. To avoid this, users should try to increase their CR by depositing more collateral or repaying their debts. However, repaying the full debt can be difficult, so there is a compensate function that allows users to repay their debts partially.

The compensate function checks whether the user's CR is larger than the liquidation CR. In scenarios where collateral prices fall suddenly and users' CR falls below the liquidation CR, users need to try to increase their CR. It's not always possible for users to have additional collateral, and repaying the full debt before maturity is not effective.

Some users will try to partially repay their debts using their USDC tokens. However, the CR will remain unchanged, and their attempt will be reverted due to their CR is still below the liquidation CR. This means that their positions could still be liquidated, leading to losses.

Proof of Concept

Imagine Bob borrows 100 USDC from Alice and has enough WETH as collateral. The WETH price suddenly falls, and Bob's CR becomes 129, while the current liquidation CR is 130. This means that Bob's debt position is liquidatable. To avoid loss, Bob tries to repay his debt partially. For example, he has 50 USDC and attempts to split his 100 USDC debt into two smaller debts of 50 USDC each, then repay one of them.

function executeCompensate(State storage state, CompensateParams calldata params) external {
    if (params.creditPositionToCompensateId == RESERVED_ID) {
        creditPositionToCompensate = state.createDebtAndCreditPositions({  // @audit, here
            lender: msg.sender,
            borrower: msg.sender,
            futureValue: amountToCompensate,
            dueDate: debtPositionToRepay.dueDate
        });
    } else {

    }

    state.reduceDebtAndCredit(
        creditPositionWithDebtToRepay.debtPositionId, params.creditPositionWithDebtToRepayId, amountToCompensate
    );
}

However, his attempt will be reverted due to the last check in the compensate function.

function compensate(CompensateParams calldata params) external payable override(ISize) whenNotPaused {
    state.validateCompensate(params);
    state.executeCompensate(params);
    state.validateUserIsNotUnderwater(msg.sender); // @audit, here
}

His CR remains 129, which is still less than 130. As a result, his 100 USDC debt position will be liquidated, leading to a loss.

Of course, in the compensate function, the user's CR can decrease when there is a fragmentation fee. However, in most cases, the CR can increase when users try to compensate using already existing credit positions or remain unchanged when creating new debt and credit positions. Therefore, it is sufficient to revert only when the CR decreases and is less than the liquidation CR. If there is no decrease in CR, this action will benefit the protocol's health.

Please add below test to the test/local/actions/Compensate.t.sol:

function test_Compensate_compensate_fail_underwater() public {
    _deposit(alice, usdc, 100e6);

    _deposit(bob, usdc, 50e6);
    _deposit(bob, weth, 129e18);

    _buyCreditLimit(alice, block.timestamp + 365 days, YieldCurveHelper.pointCurve(365 days, 0.03e18));
    uint256 debtPositionId = _sellCreditMarket(bob, alice, RESERVED_ID, 100e6, 365 days, true);
    uint256 creditPositionId = size.data().nextCreditPositionId - 1;

    _setPrice(1e18);

    vm.prank(bob);
    vm.expectRevert(
        abi.encodeWithSelector(
            Errors.USER_IS_UNDERWATER.selector, bob, 1290000000000000000
        )
    );
    size.compensate(
        CompensateParams({
            creditPositionWithDebtToRepayId: creditPositionId,
            creditPositionToCompensateId: RESERVED_ID,
            amount: 50e6
        })
    );
}

Tools Used

Recommended Mitigation Steps

Revert only when the CR decreases and is less than the liquidation CR.

Assessed type

Invalid Validation

aviggiano commented 3 months ago

This is a duplicate of https://github.com/code-423n4/2024-06-size-findings/issues/107

c4-judge commented 3 months ago

hansfriese marked the issue as satisfactory

c4-judge commented 3 months ago

hansfriese marked the issue as duplicate of #107