code-423n4 / 2023-10-badger-findings

1 stars 1 forks source link

Stormy - The new pagination feature for finding a certain Cdp by given index is used incorrectly in LeverageMacroBase, as a result the macro won't be able to correctly compare the stats of its newly opened Cdps when performing the post creation call check. #320

Closed c4-submissions closed 9 months ago

c4-submissions commented 9 months ago

Lines of code

https://github.com/code-423n4/2023-10-badger/blob/main/packages/contracts/contracts/LeverageMacroBase.sol#L118-L211

Vulnerability details

Impact

On short explanation, the function cdpOfOwnerByIdx loops through the sorted list from the lowest NICR to the biggest NICR in order to find the correct Cdp position by the provided details:

This feature is implemented in LeverageMacroBase, so when new Cdp position is opened, the user is free to compare and check its new Cdp parameters. However in order to do that the macro does the following things:

        /**
         * POST CALL CHECK FOR CREATION
         */
        if (postCheckType == PostOperationCheck.openCdp) {
            // How to get owner
            // sortedCdps.existCdpOwners(_cdpId);
            // initialCdpIndex is initialCdpIndex + 1
            bytes32 cdpId = sortedCdps.cdpOfOwnerByIndex(address(this), initialCdpIndex);

            // Check for param details
            ICdpManagerData.Cdp memory cdpInfo = cdpManager.Cdps(cdpId);
            _doCheckValueType(checkParams.expectedDebt, cdpInfo.debt);
            _doCheckValueType(checkParams.expectedCollateral, cdpInfo.coll);
            require(
                cdpInfo.status == checkParams.expectedStatus,
                "!LeverageMacroReference: openCDP status check"
            );
        }

This feature might work when opening the first Cdp, but won't work correctly when the macro already has few opened Cdp positions. The main problem occurs in the post check, when trying to find the correct Cdp position with the help of the function cdpOfOwnerByIndex, the system simply loops through the sorted list and gets the last Cdp position owned by the macro with the biggest NICR. So in order for the post check to work correctly, the system takes a guess that every newly opened Cdp by the macro will have a bigger NICR than the previous opened Cdp positions, which is unrealistic and won't be the case most of the times.

For the example and the provided POC, user opens two new Cdps and wants to check their position details:

  1. First the user opens a Cdp with an ICR of 160%, checking the param details here will be successful as its the only opened and owned Cdp by the macro.
  2. Second the user opens a Cdp with an ICR of 110%, in the post check the function cdpOfOwnerByIndex will return the last macro's Cdp position in the sorted list with the biggest NICR, which in fact will be the first opened Cdp position with ICR of 160%, as a result checking the param details for the newly opened Cdp with ICR of 110% will simply fail or be incorrect.

Proof of Concept

Step 1: Get the macro's owner and check if the owner is the wallet address.
Step 2: Prepare data for opening the first Cdp with ICR == 160%
Step 3: Approve leverage with the both Cdps collateral + liquidator rewards.
Step 4: Open the first Cdp with ICR == 160% and check param details.
Step 5: Prepare data for the opening of the second Cdp with ICR == 110%.
Step 6: Open the second Cdp with ICR == 110% and expect revert when checking the param details.
Final: Post check reverts as cdpOfOwnerByIndex returns the last macro Cdp with the biggest NICR.
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "forge-std/Test.sol";
import {eBTCBaseInvariants} from "./BaseInvariants.sol";
import {LeverageMacroReference} from "../contracts/LeverageMacroReference.sol";
import {LeverageMacroBase} from "../contracts/LeverageMacroBase.sol";
import {ICdpManagerData} from "../contracts/Interfaces/ICdpManagerData.sol";

contract LeverageMacroCdpIssue is eBTCBaseInvariants {

    address wallet = address(0xbad455);

    LeverageMacroReference macro_reference;
    LeverageMacroBase macro_base;

    function setUp() public override {
        super.setUp();

        connectCoreContracts();
        connectLQTYContractsToCore();

        macro_reference = new LeverageMacroReference(
            address(borrowerOperations),
            address(activePool),
            address(cdpManager),
            address(eBTCToken),
            address(collateral),
            address(sortedCdps),
            wallet
        );

        dealCollateral(wallet, 100e18);
    }

    function testLeverageMacroCdpIssue() public {

        vm.startPrank(wallet);

        // Step 1: Get the macro's owner and check if the owner is the wallet address
        address macroOwner = macro_reference.owner();
        assert(wallet == macroOwner);

        // Step 2: Prepare data for opening the first Cdp with ICR == 160%
        LeverageMacroBase.SwapOperation[] memory _levSwapsBefore;
        LeverageMacroBase.SwapOperation[] memory _levSwapsAfter;

        uint256 netColl = 11e18;
        uint256 grossColl = netColl + cdpManager.LIQUIDATOR_REWARD();

        uint256 debtCR = _utils.calculateBorrowAmount(
            netColl,
            priceFeedMock.fetchPrice(),
            COLLATERAL_RATIO
        );

        LeverageMacroBase.OpenCdpOperation memory _opData = LeverageMacroBase.OpenCdpOperation(
            debtCR,
            bytes32(0),
            bytes32(0),
            grossColl
        );

        bytes memory _opDataEncoded = abi.encode(_opData);

        LeverageMacroBase.LeverageMacroOperation memory operation = LeverageMacroBase
            .LeverageMacroOperation(
                address(collateral),
                grossColl,
                _levSwapsBefore,
                _levSwapsAfter,
                LeverageMacroBase.OperationType.OpenCdpOperation,
                _opDataEncoded
            );

        LeverageMacroBase.PostCheckParams memory postCheckParams = LeverageMacroBase
            .PostCheckParams({
                expectedDebt: LeverageMacroBase.CheckValueAndType({
                    value: debtCR,
                    operator: LeverageMacroBase.Operator.equal
                }),
                expectedCollateral: LeverageMacroBase.CheckValueAndType({
                    value: 11e18,
                    operator: LeverageMacroBase.Operator.equal
                }),
                cdpId: bytes32(0),
                expectedStatus: ICdpManagerData.Status.active
            });

        // Step 3: Approve leverage with the both Cdps collateral + liquidator rewards.
        collateral.approve(address(macro_reference), 22e18 + 4e17);

        // Step 4: Open the first Cdp with ICR == 160% and check param details.
        macro_reference.doOperation(
                    LeverageMacroBase.FlashLoanType.noFlashloan,
                    0,
                    operation,
                    LeverageMacroBase.PostOperationCheck.openCdp,
                    postCheckParams
                );

        // Step 5: Prepare data for the opening of the second Cdp with ICR == 110%.
        uint256 debtMCR = _utils.calculateBorrowAmount(
            netColl,
            priceFeedMock.fetchPrice(),
            MINIMAL_COLLATERAL_RATIO
        );

        LeverageMacroBase.OpenCdpOperation memory _opDataB = LeverageMacroBase.OpenCdpOperation(
            debtMCR,
            bytes32(0),
            bytes32(0),
            grossColl
        );

        bytes memory _opDataEncodedB = abi.encode(_opDataB);

        LeverageMacroBase.LeverageMacroOperation memory operationB = LeverageMacroBase
            .LeverageMacroOperation(
                address(collateral),
                grossColl,
                _levSwapsBefore,
                _levSwapsAfter,
                LeverageMacroBase.OperationType.OpenCdpOperation,
                _opDataEncodedB
            );

        LeverageMacroBase.PostCheckParams memory postCheckParamsB = LeverageMacroBase
            .PostCheckParams({
                expectedDebt: LeverageMacroBase.CheckValueAndType({
                    value: debtMCR,
                    operator: LeverageMacroBase.Operator.equal
                }),
                expectedCollateral: LeverageMacroBase.CheckValueAndType({
                    value: 11e18,
                    operator: LeverageMacroBase.Operator.equal
                }),
                cdpId: bytes32(0),
                expectedStatus: ICdpManagerData.Status.active
            });

        // Step 6: Open the second Cdp with ICR == 110% and expect revert when checking the param details.
        vm.expectRevert();
        macro_reference.doOperation(
                    LeverageMacroBase.FlashLoanType.noFlashloan,
                    0,
                    operationB,
                    LeverageMacroBase.PostOperationCheck.openCdp,
                    postCheckParamsB
                );

        vm.stopPrank();
    }
}

Tools Used

Manual review.

Recommended Mitigation Steps

In the current system, there are tons of ways to figure out what will be the correct index for the newly opened Cdp. As an example with the view function getSyncedICR, the user can get the current ICR of his Cdp positions and figure out which will be the correct index to use for the post check when opening the new Cdp.

    function getSyncedICR(bytes32 _cdpId, uint256 _price) public view returns (uint256) {
        uint256 _debt = getSyncedCdpDebt(_cdpId);
        uint256 _collShare = getSyncedCdpCollShares(_cdpId);
        return _calculateCR(_collShare, _debt, _price);
    }

My recommendation would be to remove the setup for post call check and let the user provide his own initial index, which will be used for the post creation call check.

    struct PostCheckParams {
        CheckValueAndType expectedDebt;
        CheckValueAndType expectedCollateral;
        // Used only if cdpStats || isClosed
        bytes32 cdpId;
        // Used only to check status
        ICdpManagerData.Status expectedStatus; // NOTE: THIS IS SUPERFLUOUS
        // Initial index used to find the correct Cdp position
+       uint256 initialCdpIndex;
    }
    function doOperation(
        FlashLoanType flType,
        uint256 borrowAmount,
        LeverageMacroOperation calldata operation,
        PostOperationCheck postCheckType,
        PostCheckParams calldata checkParams
    ) external {
        _assertOwner();

        // Call FL Here, then the stuff below needs to happen inside the FL
        if (operation.amountToTransferIn > 0) {
            IERC20(operation.tokenToTransferIn).safeTransferFrom(
                msg.sender,
                address(this),
                operation.amountToTransferIn
            );
        }

        // Take eBTC or stETH FlashLoan
        if (flType == FlashLoanType.eBTC) {
            IERC3156FlashLender(address(borrowerOperations)).flashLoan(
                IERC3156FlashBorrower(address(this)),
                address(ebtcToken),
                borrowAmount,
                abi.encode(operation)
            );
        } else if (flType == FlashLoanType.stETH) {
            IERC3156FlashLender(address(activePool)).flashLoan(
                IERC3156FlashBorrower(address(this)),
                address(stETH),
                borrowAmount,
                abi.encode(operation)
            );
        } else {
            // No leverage, just do the operation
            _handleOperation(operation);
        }

        /**
         * POST CALL CHECK FOR CREATION
         */
        if (postCheckType == PostOperationCheck.openCdp) {

+           bytes32 cdpId = sortedCdps.cdpOfOwnerByIndex(address(this), checkParams.initialCdpIndex);

            // Check for param details
            ICdpManagerData.Cdp memory cdpInfo = cdpManager.Cdps(cdpId);
            _doCheckValueType(checkParams.expectedDebt, cdpInfo.debt);
            _doCheckValueType(checkParams.expectedCollateral, cdpInfo.coll);
            require(
                cdpInfo.status == checkParams.expectedStatus,
                "!LeverageMacroReference: openCDP status check"
            );
        }

        // Update CDP, Ensure the stats are as intended
        if (postCheckType == PostOperationCheck.cdpStats) {
            ICdpManagerData.Cdp memory cdpInfo = cdpManager.Cdps(checkParams.cdpId);

            _doCheckValueType(checkParams.expectedDebt, cdpInfo.debt);
            _doCheckValueType(checkParams.expectedCollateral, cdpInfo.coll);
            require(
                cdpInfo.status == checkParams.expectedStatus,
                "!LeverageMacroReference: adjustCDP status check"
            );
        }

        // Post check type: Close, ensure it has the status we want
        if (postCheckType == PostOperationCheck.isClosed) {
            ICdpManagerData.Cdp memory cdpInfo = cdpManager.Cdps(checkParams.cdpId);

            require(
                cdpInfo.status == checkParams.expectedStatus,
                "!LeverageMacroReference: closeCDP status check"
            );
        }

        // Sweep here if it's Reference, do not if it's delegate
        if (willSweep) {
            sweepToCaller();
        }
    }

Assessed type

Other

bytes032 commented 9 months ago

https://gist.github.com/GalloDaSballo/a0f9766bf7bac391f49d2d167e947de0#incorrect-sorting-due-to-pending-debt-and-yield CleanShot 2023-11-17 at 9  26 17

c4-pre-sort commented 9 months ago

bytes032 marked the issue as high quality report

c4-pre-sort commented 9 months ago

bytes032 marked the issue as primary issue

c4-pre-sort commented 9 months ago

bytes032 marked the issue as duplicate of #152

c4-judge commented 9 months ago

jhsagd76 marked the issue as satisfactory

KristianApostolov commented 9 months ago

l think this issue should be selected for the report due to the proof of concept, which is missing in the other reports. @jhsagd76

jhsagd76 commented 9 months ago

Thank you for your excellent work. This report and POC are indeed outstanding. However, the current primay issue also clearly highlights the vulnerabilities and relevant code. I believe it would be unfair to switch to this directly.

However, additional incentive rules for POC are currently under discussion. I think you are doing the right thing, so please keep it up. Thank you very much for your excellent report!