28858 - [SC - Insight] Execution of SortedCpds while command may cause...

Submitted on Feb 29th 2024 at 01:10:58 UTC by @cryptoticky for Boost | eBTC

Report ID: #28858

Report type: Smart Contract

Report severity: Insight

Target: https://github.com/ebtc-protocol/ebtc/blob/release-0.7/packages/contracts/contracts/SortedCdps.sol

Impacts:

  • Unbounded gas consumption

Description

Brief/Intro

Execution of SortedCpd's while command may cause excessive gas consumption.

Vulnerability Details

The codes below can result in significant gas costs.

CdpManager.sol:373-376 lines

while (currentBorrower != address(0) && getSyncedICR(_cId, totals.price) < MCR) {
    _cId = sortedCdps.getPrev(_cId);
    currentBorrower = sortedCdps.getOwnerAddress(_cId);
}

HintHelpers.sol:68-74 lines

while (
    vars.currentCdpUser != address(0) &&
    cdpManager.getSyncedICR(vars.currentCdpId, _price) < MCR
) {
    vars.currentCdpId = sortedCdps.getPrev(vars.currentCdpId);
    vars.currentCdpUser = sortedCdps.getOwnerAddress(vars.currentCdpId);
}

If there are a significant number of CDPs with ICRs smaller than MCRs, the user must pay a significant gas cost and out of gas exception can occur, resulting in gas loss. In the worst-case scenario, the gas cost may exceed the block gas limit and the protocol will not be able to operate normally. However, the latter is theoretically possible, but will not happen in reality.

In this report, the former is explained and solutions are presented.

Let's look at the cases (no CDP and 100 CDPs) : ICR < MCR

You can create PoC_CDPManager.redemptions.gaslimit.t.sol file in foundry_test folder.

And run this in terminal.

forge test -vvvv --match-contract PoC_CDPManagerRedemptionsGasLimitTest.

// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "forge-std/Test.sol";
import "../contracts/Dependencies/EbtcMath.sol";
import {eBTCBaseInvariants} from "./BaseInvariants.sol";

contract PoC_CDPManagerRedemptionsGasLimitTest is eBTCBaseInvariants {
    address user;


    function setUp() public override {
        super.setUp();
        connectCoreContracts();
        connectLQTYContractsToCore();
        vm.warp(3 weeks);
    }

    function test_PoC1SingleRedemptionGasFee() public {
        console.log("\n");

        console.log("========= Configration ==========");
        user = _utils.getNextUserAddress();


        bytes32 cdpId0;
        bytes32 cdpId1;
        bytes32 cdpId2;

        console.log("\n");
        console.log("------- Redemption when there is no CDP (ICR < MCR) -------");

        // --- open cdps ---
        (, cdpId1) = _singleCdpSetupWithICR(user, 130e16);
        (, cdpId2) = _singleCdpSetupWithICR(user, 140e16);

        console.log("- Redeem CDP1 fully");
        bytes32 dummyId = sortedCdps.dummyId();
        uint256 _redeemedDebt = cdpManager.getCdpDebt(cdpId1);
        (, uint256 partialRedemptionHintNICR, , ) = hintHelpers
            .getRedemptionHints(_redeemedDebt, priceFeedMock.fetchPrice(), 0);
        _syncSystemDebtTwapToSpotValue();
        vm.startPrank(user);
        cdpManager.redeemCollateral(
            _redeemedDebt,
            dummyId,
            cdpId1,
            cdpId1,
            partialRedemptionHintNICR,
            0,
            1e18
        );
        vm.stopPrank();

        console.log("Please check gas amounts in the test report!!!");

        console.log("The report must show these result!");
        console.log("--- Result ---");
        console.log("[283703] AccruableCdpManager::redeemCollateral");
    }

    function test_PoC2SingleRedemptionGasFee() public {
        console.log("\n");

        console.log("========= Configration ==========");
        user = _utils.getNextUserAddress();


        console.log("\n");
        console.log("------- Redemption when there are 100 CDPs (ICR < MCR) -------");

        // --- open cdps ---
        bytes32 cdpId1;
        for (uint256 i; i < 1000; i++) {
            (, cdpId1) = _singleCdpSetupWithICR(user, 130e16);
        }

        uint256 price = priceFeedMock.getPrice();
        price = price * 10 / 12;
        priceFeedMock.setPrice(price);